-
-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ubisoft OAuth integration #56
Merged
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
aab5d38
Add trackmania login button and URL
MKuijpers 3efcd85
Add basic TM token exchange and connect front-end
MKuijpers 8254741
Add session secrets and storage of sessions
MKuijpers f3f6030
Add session deletion and proper logout requests
MKuijpers a5fa7eb
Use HttpOnly cookie for session
MKuijpers 2dd5c7d
Rename sessionSecret to sessionId
MKuijpers 7cab746
Store user info in AuthContext
MKuijpers 64a3bd9
Logout and update user using AuthContext
MKuijpers d00a24e
Add user auth middleware on server
MKuijpers 8b4010f
Remove unnecessary TODOs
MKuijpers a83c9e9
Remove Check Auth button
MKuijpers aa18353
Move UserDisplay to common components
MKuijpers e3ff8a4
User router.back() instead of redirecting to the homepage
MKuijpers 06b57bd
Add login button on map page
MKuijpers 860e8b2
Add proper redirects after login
davidbmaier 1535b73
Add and check random state in auth URL
MKuijpers 01fc4c4
Remove state after retrieving it + add log for invalid state
MKuijpers 2d01b33
Open ubisoft auth in new window
MKuijpers ba2ee69
Move auth popup window code into UserDisplay component
MKuijpers 70090f4
Extract and clean window popup code
MKuijpers 50ad48e
General code cleanup based on PR comments
MKuijpers fc3a3c7
Update depricated client check
MKuijpers 2ddc7cb
Replace deprecated querystring with URLSearchParams
MKuijpers 24a28ab
Change 'next(); return;' to 'return next();'
MKuijpers bf5053c
Remove HttpOnly and check sessionId cookie for /me requests
MKuijpers aa5d2b2
Refactor auth flow code and simplify window opening
MKuijpers 7f15824
Add return at the end of async function (thanks linter)
MKuijpers File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
NEXT_PUBLIC_API_URL=http://localhost | ||
NEXT_PUBLIC_ANALYTICS_ID= | ||
|
||
NEXT_PUBLIC_CLIENT_ID=<CLIENT_ID from api.trackmania.com> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import React, { useContext } from 'react'; | ||
import { Button, message } from 'antd'; | ||
import { AuthContext } from '../../lib/contexts/AuthContext'; | ||
|
||
const LoginButton = ({ onClick } :{onClick: () => void}) => ( | ||
<Button type="primary" onClick={onClick}> | ||
Login with Ubisoft | ||
</Button> | ||
); | ||
|
||
const LogoutButton = ({ onClick } :{onClick: () => void}) => ( | ||
<Button | ||
type="primary" | ||
danger | ||
style={{ marginLeft: '10px' }} | ||
onClick={onClick} | ||
> | ||
Logout | ||
</Button> | ||
); | ||
|
||
const UserDisplay = () => { | ||
const { user, startAuthFlow, logoutUser } = useContext(AuthContext); | ||
|
||
const onLogout = async () => { | ||
try { | ||
logoutUser(); | ||
} catch (e) { | ||
message.error('Something went wrong while logging out.'); | ||
} | ||
}; | ||
|
||
return user === undefined | ||
? <LoginButton onClick={startAuthFlow} /> | ||
: ( | ||
<div className="flex flex-row items-center"> | ||
{`Welcome, ${user.displayName}!`} | ||
<LogoutButton | ||
onClick={onLogout} | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
export default UserDisplay; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import axios from 'axios'; | ||
|
||
const getRedirectUri = () => { | ||
if (typeof window === 'undefined') { | ||
// Avoid some nextjs compilation errors regarding window being undefined | ||
return undefined; | ||
} | ||
return `${window.location.origin}/auth_redirect`; | ||
}; | ||
|
||
export const generateAuthUrl = (state: string): string => { | ||
const url = 'https://api.trackmania.com/oauth/authorize'; | ||
|
||
const params = { | ||
response_type: 'code', | ||
client_id: process.env.NEXT_PUBLIC_CLIENT_ID, | ||
redirect_uri: getRedirectUri(), | ||
state, | ||
}; | ||
|
||
return axios.getUri({ url, params }); | ||
}; | ||
|
||
interface AuthorizationResponse { | ||
displayName: string; | ||
accountId: string; | ||
} | ||
export const authorizeWithAccessCode = async (accessCode: string): Promise<AuthorizationResponse> => { | ||
const url = `${process.env.NEXT_PUBLIC_API_URL}/authorize`; | ||
|
||
const params = { | ||
code: accessCode, | ||
redirect_uri: getRedirectUri(), | ||
}; | ||
|
||
// TODO: use custom axios instance with default config for withCredentials | ||
const { data } = await axios.post(url, params, { withCredentials: true }); | ||
|
||
return data; | ||
}; | ||
|
||
export interface UserInfo { | ||
displayName: string; | ||
accountId: string; | ||
} | ||
export const fetchLoggedInUser = async (): Promise<UserInfo | undefined> => { | ||
const url = `${process.env.NEXT_PUBLIC_API_URL}/me`; | ||
|
||
const hasSessionCookie = document.cookie | ||
.split(';') | ||
.filter((cookie) => cookie.trim().startsWith('sessionId=')) | ||
.length > 0; | ||
|
||
if (!hasSessionCookie) { | ||
return undefined; | ||
} | ||
|
||
try { | ||
// TODO: use custom axios instance with default config for withCredentials | ||
const { data } = await axios.post(url, {}, { withCredentials: true }); | ||
return data; | ||
} catch (e) { | ||
// TODO: find solution for being logged out on a server error? | ||
return undefined; | ||
} | ||
}; | ||
|
||
export const logout = async (): Promise<void> => { | ||
const url = `${process.env.NEXT_PUBLIC_API_URL}/logout`; | ||
|
||
// TODO: use custom axios instance with default config for withCredentials | ||
await axios.post(url, {}, { withCredentials: true }); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { useRouter } from 'next/router'; | ||
import React, { | ||
createContext, useCallback, useEffect, useState, | ||
} from 'react'; | ||
import { | ||
authorizeWithAccessCode, fetchLoggedInUser, generateAuthUrl, logout, UserInfo, | ||
} from '../api/auth'; | ||
import openAuthWindow from '../utils/authPopup'; | ||
|
||
export interface AuthContextProps { | ||
user?: UserInfo, | ||
setUser: (user?: UserInfo) => void, | ||
logoutUser: () => Promise<void> | ||
startAuthFlow: () => void | ||
} | ||
|
||
export const AuthContext = createContext<AuthContextProps>({ | ||
user: undefined, | ||
setUser: (user?: UserInfo) => {}, | ||
logoutUser: async () => {}, | ||
startAuthFlow: () => {}, | ||
}); | ||
|
||
export const AuthProvider = ({ children }: any): JSX.Element => { | ||
const [user, setUser] = useState<UserInfo>(); | ||
const { asPath } = useRouter(); | ||
|
||
useEffect(() => { | ||
updateLoggedInUser(); | ||
}, [asPath]); | ||
|
||
const updateLoggedInUser = async () => { | ||
const me = await fetchLoggedInUser(); | ||
if (me === undefined) { | ||
setUser(undefined); | ||
} else if (me?.accountId !== user?.accountId) { | ||
setUser(me); | ||
} | ||
}; | ||
|
||
const startAuthFlow = () => { | ||
// Generate and store random string as state | ||
const state = Math.random().toString(36).substring(2); // 11 random lower-case alpha-numeric characters | ||
localStorage.setItem('state', state); | ||
|
||
// Remove any existing event listeners | ||
window.removeEventListener('message', receiveAuthEvent); | ||
|
||
openAuthWindow(generateAuthUrl(state), 'Login with Ubisoft'); | ||
|
||
// Add the listener for receiving a message from the popup | ||
window.addEventListener('message', receiveAuthEvent, false); | ||
}; | ||
|
||
const receiveAuthEvent = useCallback(async (event: any) => { | ||
if (event.origin !== window.origin) { | ||
return; | ||
} | ||
|
||
const { data } = event; | ||
const { source, code, state } = data; | ||
if (source !== 'ubi-login-redirect') { | ||
return; | ||
} | ||
|
||
// We received a message from the auth window, remove this listener | ||
window.removeEventListener('message', receiveAuthEvent); | ||
|
||
if (code === undefined || code === null || typeof code !== 'string') { | ||
return; | ||
} | ||
if (state === undefined || state === null || typeof state !== 'string') { | ||
return; | ||
} | ||
|
||
const storedState = localStorage.getItem('state'); | ||
localStorage.removeItem('state'); | ||
if (storedState !== state) { | ||
console.log(`Stored state (${storedState}) did not match incoming state (${state})`); | ||
return; | ||
} | ||
|
||
try { | ||
const userInfo = await authorizeWithAccessCode(code); | ||
setUser(userInfo); | ||
} catch (e) { | ||
console.log(e); | ||
} | ||
}, [setUser]); | ||
|
||
const logoutUser = async () => { | ||
try { | ||
await logout(); | ||
setUser(undefined); | ||
} catch (e) { | ||
// If error code is Unauthorized (so no user is logged in), set user to undefined | ||
// This should only happen when manually deleting the session cookie | ||
if (e.response.status === 401) { | ||
setUser(undefined); | ||
} else { | ||
throw e; | ||
} | ||
} | ||
}; | ||
|
||
return ( | ||
<AuthContext.Provider | ||
value={{ | ||
user, | ||
setUser, | ||
logoutUser, | ||
startAuthFlow, | ||
}} | ||
> | ||
{children} | ||
</AuthContext.Provider> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// https://dev.to/dinkydani21/how-we-use-a-popup-for-google-and-outlook-oauth-oci | ||
let windowReference: Window | null = null; | ||
let previousUrl: string | undefined; | ||
|
||
const openAuthWindow = ( | ||
url: string, | ||
name: string, | ||
) => { | ||
const windowFeatures = 'toolbar=no, menubar=no, width=600, height=700, top=100, left=100'; | ||
|
||
if (windowReference === null || windowReference.closed || previousUrl !== url) { | ||
windowReference = window.open(url, name, windowFeatures); | ||
windowReference?.focus(); | ||
} | ||
|
||
previousUrl = url; | ||
}; | ||
|
||
export default openAuthWindow; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to make sure we handle this well everywhere - especially since this currently is what determines if the user is logged in.
Also, we might want to find a way to store our login state somewhere persistent - so we know if sending this request even makes sense (so we can avoid useless calls that end in 401s anyway).