Skip to content

Commit

Permalink
feat: add shouldTryRefresh plus self-review and test related fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
porcellus committed Aug 18, 2024
1 parent 905b5cd commit 9f7866c
Show file tree
Hide file tree
Showing 21 changed files with 613 additions and 168 deletions.
20 changes: 13 additions & 7 deletions lib/build/recipe/oauth2provider/api/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../../../utils");
const set_cookie_parser_1 = __importDefault(require("set-cookie-parser"));
const session_1 = __importDefault(require("../../session"));
const error_1 = __importDefault(require("../../../recipe/session/error"));
async function authGET(apiImplementation, options, userContext) {
if (apiImplementation.authGET === undefined) {
return false;
}
const origURL = options.req.getOriginalURL();
const splitURL = origURL.split("?");
const params = new URLSearchParams(splitURL[1]);
let session;
let session, shouldTryRefresh;
try {
session = await session_1.default.getSession(options.req, options.res, { sessionRequired: false }, userContext);
} catch (_a) {
// We ignore this here since the authGET is called from the auth endpoint which is used in full-page redirections
// Returning a 401 would break the sign-in flow.
// In theory we could serve some JS that handles refreshing and retrying, but this is not implemented.
// What we do is that the auth endpoint will redirect to the login page, and the login page handles refreshing and
// redirect to the auth endpoint again. This is not optimal, but it works for now.
shouldTryRefresh = false;
} catch (error) {
session = undefined;
if (error_1.default.isErrorFromSuperTokens(error) && error.type === error_1.default.TRY_REFRESH_TOKEN) {
shouldTryRefresh = true;
} else {
// This should generally not happen, but we can handle this as if the session is not present,
// because then we redirect to the frontend, which should handle the validation error
shouldTryRefresh = false;
}
}
let response = await apiImplementation.authGET({
options,
params: Object.fromEntries(params.entries()),
cookie: options.req.getHeaderValue("cookie"),
session,
shouldTryRefresh,
userContext,
});
if ("redirectTo" in response) {
Expand Down
8 changes: 6 additions & 2 deletions lib/build/recipe/oauth2provider/api/implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("./utils");
function getAPIImplementation() {
return {
loginGET: async ({ loginChallenge, options, session, userContext }) => {
loginGET: async ({ loginChallenge, options, session, shouldTryRefresh, userContext }) => {
const response = await utils_1.loginGET({
recipeImplementation: options.recipeImplementation,
loginChallenge,
session,
shouldTryRefresh,
isDirectCall: true,
userContext,
});
Expand All @@ -30,10 +31,11 @@ function getAPIImplementation() {
cookie: options.req.getHeaderValue("cookie"),
recipeImplementation: options.recipeImplementation,
session,
shouldTryRefresh,
userContext,
});
},
authGET: async ({ options, params, cookie, session, userContext }) => {
authGET: async ({ options, params, cookie, session, shouldTryRefresh, userContext }) => {
const response = await options.recipeImplementation.authorization({
params,
cookies: cookie,
Expand All @@ -45,6 +47,7 @@ function getAPIImplementation() {
recipeImplementation: options.recipeImplementation,
cookie,
session,
shouldTryRefresh,
userContext,
});
},
Expand All @@ -63,6 +66,7 @@ function getAPIImplementation() {
return {
status: "OK",
info: {
clientId: client.clientId,
clientName: client.clientName,
tosUri: client.tosUri,
policyUri: client.policyUri,
Expand Down
12 changes: 10 additions & 2 deletions lib/build/recipe/oauth2provider/api/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,25 @@ const set_cookie_parser_1 = __importDefault(require("set-cookie-parser"));
const utils_1 = require("../../../utils");
const session_1 = __importDefault(require("../../session"));
const error_1 = __importDefault(require("../../../error"));
const error_2 = __importDefault(require("../../../recipe/session/error"));
async function login(apiImplementation, options, userContext) {
var _a;
if (apiImplementation.loginGET === undefined) {
return false;
}
let session;
let session, shouldTryRefresh;
try {
session = await session_1.default.getSession(options.req, options.res, { sessionRequired: false }, userContext);
} catch (_b) {
shouldTryRefresh = false;
} catch (error) {
// We can handle this as if the session is not present, because then we redirect to the frontend,
// which should handle the validation error
session = undefined;
if (error_1.default.isErrorFromSuperTokens(error) && error.type === error_2.default.TRY_REFRESH_TOKEN) {
shouldTryRefresh = true;
} else {
shouldTryRefresh = false;
}
}
const loginChallenge =
(_a = options.req.getKeyValueFromQuery("login_challenge")) !== null && _a !== void 0
Expand All @@ -50,6 +57,7 @@ async function login(apiImplementation, options, userContext) {
options,
loginChallenge,
session,
shouldTryRefresh,
userContext,
});
if ("status" in response) {
Expand Down
4 changes: 4 additions & 0 deletions lib/build/recipe/oauth2provider/api/utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { RecipeInterface } from "../types";
export declare function loginGET({
recipeImplementation,
loginChallenge,
shouldTryRefresh,
session,
setCookie,
isDirectCall,
Expand All @@ -13,6 +14,7 @@ export declare function loginGET({
recipeImplementation: RecipeInterface;
loginChallenge: string;
session?: SessionContainerInterface;
shouldTryRefresh: boolean;
setCookie?: string;
userContext: UserContext;
isDirectCall: boolean;
Expand All @@ -24,6 +26,7 @@ export declare function handleInternalRedirects({
response,
recipeImplementation,
session,
shouldTryRefresh,
cookie,
userContext,
}: {
Expand All @@ -33,6 +36,7 @@ export declare function handleInternalRedirects({
};
recipeImplementation: RecipeInterface;
session?: SessionContainerInterface;
shouldTryRefresh: boolean;
cookie?: string;
userContext: UserContext;
}): Promise<{
Expand Down
52 changes: 42 additions & 10 deletions lib/build/recipe/oauth2provider/api/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@ const constants_2 = require("../constants");
const set_cookie_parser_1 = __importDefault(require("set-cookie-parser"));
// API implementation for the loginGET function.
// Extracted for use in both apiImplementation and handleInternalRedirects.
async function loginGET({ recipeImplementation, loginChallenge, session, setCookie, isDirectCall, userContext }) {
async function loginGET({
recipeImplementation,
loginChallenge,
shouldTryRefresh,
session,
setCookie,
isDirectCall,
userContext,
}) {
var _a, _b;
const loginRequest = await recipeImplementation.getLoginRequest({
challenge: loginChallenge,
Expand Down Expand Up @@ -80,6 +88,30 @@ async function loginGET({ recipeImplementation, loginChallenge, session, setCook
});
return { redirectTo: accept.redirectTo, setCookie };
}
const appInfo = supertokens_1.default.getInstanceOrThrowError().appInfo;
const websiteDomain = appInfo
.getOrigin({
request: undefined,
userContext: userContext,
})
.getAsStringDangerous();
const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous();
if (shouldTryRefresh) {
const websiteDomain = appInfo
.getOrigin({
request: undefined,
userContext: userContext,
})
.getAsStringDangerous();
const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous();
const queryParamsForTryRefreshPage = new URLSearchParams({
loginChallenge,
});
return {
redirectTo: websiteDomain + websiteBasePath + `/try-refresh?${queryParamsForTryRefreshPage.toString()}`,
setCookie,
};
}
if (promptParam === "none") {
const reject = await recipeImplementation.rejectLoginRequest({
challenge: loginChallenge,
Expand All @@ -92,14 +124,6 @@ async function loginGET({ recipeImplementation, loginChallenge, session, setCook
});
return { redirectTo: reject.redirectTo, setCookie };
}
const appInfo = supertokens_1.default.getInstanceOrThrowError().appInfo;
const websiteDomain = appInfo
.getOrigin({
request: undefined,
userContext: userContext,
})
.getAsStringDangerous();
const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous();
const queryParamsForAuthPage = new URLSearchParams({
loginChallenge,
});
Expand Down Expand Up @@ -160,7 +184,14 @@ function isInternalRedirect(redirectTo) {
// In the OAuth2 flow, we do several internal redirects. These redirects don't require a frontend-to-api-server round trip.
// If an internal redirect is identified, it's handled directly by this function.
// Currently, we only need to handle redirects to /oauth/login and /oauth/auth endpoints.
async function handleInternalRedirects({ response, recipeImplementation, session, cookie = "", userContext }) {
async function handleInternalRedirects({
response,
recipeImplementation,
session,
shouldTryRefresh,
cookie = "",
userContext,
}) {
var _a;
if (!isInternalRedirect(response.redirectTo)) {
return response;
Expand All @@ -183,6 +214,7 @@ async function handleInternalRedirects({ response, recipeImplementation, session
recipeImplementation,
loginChallenge,
session,
shouldTryRefresh,
setCookie: response.setCookie,
isDirectCall: false,
userContext,
Expand Down
25 changes: 17 additions & 8 deletions lib/build/recipe/oauth2provider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,38 @@ const utils_1 = require("../../utils");
const recipe_1 = __importDefault(require("./recipe"));
class Wrapper {
static async getOAuth2Client(clientId, userContext) {
return await recipe_1.default
.getInstanceOrThrowError()
.recipeInterfaceImpl.getOAuth2Client({ clientId }, utils_1.getUserContext(userContext));
return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getOAuth2Client({
clientId,
userContext: utils_1.getUserContext(userContext),
});
}
static async getOAuth2Clients(input, userContext) {
return await recipe_1.default
.getInstanceOrThrowError()
.recipeInterfaceImpl.getOAuth2Clients(input, utils_1.getUserContext(userContext));
.recipeInterfaceImpl.getOAuth2Clients(
Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(userContext) })
);
}
static async createOAuth2Client(input, userContext) {
return await recipe_1.default
.getInstanceOrThrowError()
.recipeInterfaceImpl.createOAuth2Client(input, utils_1.getUserContext(userContext));
.recipeInterfaceImpl.createOAuth2Client(
Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(userContext) })
);
}
static async updateOAuth2Client(input, userContext) {
return await recipe_1.default
.getInstanceOrThrowError()
.recipeInterfaceImpl.updateOAuth2Client(input, utils_1.getUserContext(userContext));
.recipeInterfaceImpl.updateOAuth2Client(
Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(userContext) })
);
}
static async deleteOAuth2Client(input, userContext) {
return await recipe_1.default
.getInstanceOrThrowError()
.recipeInterfaceImpl.deleteOAuth2Client(input, utils_1.getUserContext(userContext));
.recipeInterfaceImpl.deleteOAuth2Client(
Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(userContext) })
);
}
static validateOAuth2AccessToken(token, requirements, checkDatabase, userContext) {
return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.validateOAuth2AccessToken({
Expand All @@ -72,7 +81,7 @@ class Wrapper {
let authorizationHeader = undefined;
const normalisedUserContext = utils_1.getUserContext(userContext);
const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl;
const res = await recipeInterfaceImpl.getOAuth2Client({ clientId }, normalisedUserContext);
const res = await recipeInterfaceImpl.getOAuth2Client({ clientId, userContext: normalisedUserContext });
if (res.status !== "OK") {
throw new Error(`Failed to get OAuth2 client with id ${clientId}: ${res.error}`);
}
Expand Down
18 changes: 17 additions & 1 deletion lib/build/recipe/oauth2provider/recipe.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import RecipeModule from "../../recipeModule";
import { APIHandled, HTTPMethod, JSONObject, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types";
import {
APIInterface,
PayloadBuilderFunction,
RecipeInterface,
TypeInput,
TypeNormalisedInput,
Expand All @@ -16,6 +17,7 @@ import { User } from "../../user";
export default class Recipe extends RecipeModule {
static RECIPE_ID: string;
private static instance;
private accessTokenBuilders;
private idTokenBuilders;
private userInfoBuilders;
config: TypeNormalisedInput;
Expand All @@ -28,6 +30,9 @@ export default class Recipe extends RecipeModule {
static init(config?: TypeInput): RecipeListFunction;
static reset(): void;
addUserInfoBuilderFromOtherRecipe: (userInfoBuilderFn: UserInfoBuilderFunction) => void;
addAccessTokenBuilderFromOtherRecipe: (accessTokenBuilders: PayloadBuilderFunction) => void;
addIdTokenBuilderFromOtherRecipe: (idTokenBuilder: PayloadBuilderFunction) => void;
saveTokensForHook: (sessionHandle: string, idToken: JSONObject, accessToken: JSONObject) => void;
getAPIsHandled(): APIHandled[];
handleAPIRequest: (
id: string,
Expand All @@ -41,7 +46,18 @@ export default class Recipe extends RecipeModule {
handleError(error: error, _: BaseRequest, __: BaseResponse, _userContext: UserContext): Promise<void>;
getAllCORSHeaders(): string[];
isErrorFromThisRecipe(err: any): err is error;
getDefaultIdTokenPayload(user: User, scopes: string[], userContext: UserContext): Promise<JSONObject>;
getDefaultAccessTokenPayload(
user: User,
scopes: string[],
sessionHandle: string,
userContext: UserContext
): Promise<JSONObject>;
getDefaultIdTokenPayload(
user: User,
scopes: string[],
sessionHandle: string,
userContext: UserContext
): Promise<JSONObject>;
getDefaultUserInfoPayload(
user: User,
accessTokenPayload: JSONObject,
Expand Down
Loading

0 comments on commit 9f7866c

Please sign in to comment.