Skip to content

Commit

Permalink
added refresh token support
Browse files Browse the repository at this point in the history
  • Loading branch information
toggm committed Jan 27, 2025
1 parent 298cda8 commit a86fc63
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 31 deletions.
6 changes: 3 additions & 3 deletions backend/app/controllers/ControllerSecurity.scala
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ trait TokenSecurity extends Logging {
private def deserializeJwtSession(token: String) = {
JwtSession.deserialize(token,
JwtOptions(
signature = false,
notBefore = false,
expiration = false,
signature = true,
notBefore = true,
expiration = true,
leeway = 60
))
}
Expand Down
1 change: 0 additions & 1 deletion backend/app/models/ExtendedJwtSession.scala
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ object ExtendedJwtSession {
audience = Some(Set(issuer)),
expiration =
Some(DateTime.now().plus(tokenLifespan.toMillis).getMillis),
notBefore = Some(DateTime.now().getMillis),
)) ++ (
EMAIL_CLAIM -> user.email,
GIVEN_NAME_CLAIM -> user.firstName,
Expand Down
2 changes: 1 addition & 1 deletion backend/app/models/OAuthAccessToken.scala
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ case class OAuthAccessToken(id: OAuthAccessTokenId,
def toAccessToken: AccessToken =
AccessToken(
token = accessToken,
refreshToken = refreshToken: Option[String],
refreshToken = refreshToken,
scope = scope,
lifeSeconds = Some(expiresIn),
createdAt = createdAt.toDate
Expand Down
22 changes: 14 additions & 8 deletions frontend/src/lib/api/hooks/useTokensWithAxiosRequests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,20 @@ export const getRequestHeaders = (token?: string) => {
};

export const useTokensWithAxiosRequests = () => {
const session = useSession();
const isClient = useIsClient();
const token = session?.data?.accessToken;
const axiosConfig = getRequestHeaders(token);
if (!token) {
logger.error('[useAxiosToken][NoTokenSetOnServerRequest]', isClient, session);
const session = useSession();
if (isClient) {
const token = session?.data?.accessToken;
const axiosConfig = getRequestHeaders(token);
if (!token) {
logger.error('[useAxiosToken][NoTokenSetOnRequest]', isClient, session.data, session.status);
}
return {
axiosConfig: axiosConfig,
};
} else {
return {
axiosConfig: undefined,
};
}
return {
axiosConfig: axiosConfig,
};
};
3 changes: 1 addition & 2 deletions frontend/src/lib/api/lasiusAxiosInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const lasiusAxiosInstance = <T>(
if (Axios.isCancel(error)) {
logger.info('[lasiusAxiosInstance][RequestCanceled]', error.message);
} else if (error.response.status === 401) {
logger.info('[lasiusAxiosInstance][Unauthorized]', {
logger.error('[lasiusAxiosInstance][Unauthorized]', {
path: error.request.pathname,
message: error.data,
});
Expand All @@ -63,7 +63,6 @@ export const lasiusAxiosInstance = <T>(
window.location.pathname !== '/login' &&
window.location.pathname !== '/'
) {
// TODO remove again
//await removeAccessibleCookies();
//await signOut();
} else {
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ const App = ({
logger.info('[App][UserNotLoggedIn]');
store.dispatch({ type: 'reset' });
await removeAccessibleCookies();
} else {
logger.info('[App][UserLoggedIn]', session);
}
}, [lasiusIsLoggedIn]);

Expand Down Expand Up @@ -182,7 +184,7 @@ App.getInitialProps = async ({
let profile = null;
const accessToken = session?.accessToken;

if (accessToken) {
if (accessToken) {
try {
profile = await getUserProfile(getRequestHeaders(accessToken));
} catch (error) {
Expand Down
99 changes: 84 additions & 15 deletions frontend/src/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*
*/

import NextAuth, { NextAuthOptions } from 'next-auth';
import NextAuth, { JWT, NextAuthOptions } from 'next-auth';
import { NextApiRequest, NextApiResponse } from 'next';
import { logger } from 'lib/logger';
import { OAuthConfig } from 'next-auth/providers';
Expand All @@ -28,7 +28,12 @@ const internalProvider: OAuthConfig<any> = {
version: '2.0',
type: 'oauth',
// redirect to local login page
authorization: 'http://localhost:3000/internal_oauth',
authorization: {
url: 'http://localhost:3000/internal_oauth',
params: {
scope: 'profile openid email',
},
},
token: 'http://localhost:3000/backend/oauth2/access_token',
userinfo: 'http://localhost:3000/backend/oauth2/profile',
clientId: process.env.LASIUS_OAUTH_CLIENT_ID,
Expand All @@ -46,6 +51,57 @@ const internalProvider: OAuthConfig<any> = {
},
};

/**
* Takes a token, and returns a new token with updated
* `accessToken` and `accessTokenExpires`. If an error occurs,
* returns the old token and an error property
*/
async function refreshAccessToken(token) {
try {
// TODO: support other providers
const url =
'http://localhost:3000/backend/oauth2/access_token?' +
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: token.refreshToken,
});

const response = await fetch(url, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: Buffer.from(
'Basic ' +
process.env.LASIUS_OAUTH_CLIENT_ID +
':' +
process.env.LASIUS_OAUTH_CLIENT_SECRET,
'binary'
).toString('base64'),
},
method: 'POST',
});

const refreshedTokens = await response.json();

if (!response.ok) {
throw refreshedTokens;
}

return {
...token,
accessToken: refreshedTokens.access_token,
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, // Fall back to old refresh token
};
} catch (error) {
console.log('[NextAuth][RefreshAccessTokenError]', error);

return {
...token,
error: 'RefreshAccessTokenError',
};
}
}

const nextAuthOptions = (): NextAuthOptions => {
return {
debug: true,
Expand All @@ -63,22 +119,35 @@ const nextAuthOptions = (): NextAuthOptions => {
},
callbacks: {
async session({ session, token }) {
if (token) {
session.accessToken = token.accessToken;
}
return {
...session,
console.log('[NextAuth][session]', session, token);
session.user = token.user;
session.accessToken = token.accessToken;
session.error = token.error;

user: { ...session.user, image: undefined },
};
return session;
},
async jwt({ token, user }) {
// the user object is what returned from the Credentials login, it has `accessToken` from the server `/login` endpoint
// assign the accessToken to the `token` object, so it will be available on the `session` callback
if (user) {
token.accessToken = user.accessToken;
async jwt({ token, user, account, profile }) {
console.log('[NextAuth][jwt][token]', token);
console.log('[NextAuth][jwt][user]', user);
console.log('[NextAuth][jwt][account]', account);
console.log('[NextAuth][jwt][profile]', profile);
// Initial sign in
if (account && user) {
return {
accessToken: account.access_token,
accessTokenExpires: Date.now() + account.expires_in * 1000,
refreshToken: account.refresh_token,
user,
};
}

// Return previous token if the access token has not expired yet or has no expiration set
if (!token.accessTokenExpires || Date.now() < token.accessTokenExpires) {
return token;
}
return token;

// Access token has expired, try to update it
return refreshAccessToken(token);
},
},
events: {
Expand Down

0 comments on commit a86fc63

Please sign in to comment.