Skip to content

Commit

Permalink
feat: Add OAuth2Client recipe (#877)
Browse files Browse the repository at this point in the history
* feat: add initial oauth2 client apis

* feat: Add an api to get login info

* fix: merge issues and FE path

* fix: WIP fix for CSRF and redirection issues

* fix: OAuth2 fixes and test-server updates (#871)

* feat: update oauth2 login info endpoint types to match our general patterns

* fix: make login flow work

* fix: circular dependency

* feat: Add OAuth2Client recipe

* fix: PR changes

* fix: PR changes

* fix: PR changes

* fix: use correct userContext type

---------

Co-authored-by: Mihaly Lengyel <[email protected]>
  • Loading branch information
anku255 and porcellus authored Jul 23, 2024
1 parent 2fd8ef4 commit e84eb49
Show file tree
Hide file tree
Showing 53 changed files with 2,196 additions and 317 deletions.
2 changes: 1 addition & 1 deletion lib/build/recipe/jwt/api/implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function getAPIImplementation() {
if (resp.validityInSeconds !== undefined) {
options.res.setHeader("Cache-Control", `max-age=${resp.validityInSeconds}, must-revalidate`, false);
}
const oauth2 = require("../../oauth2").getInstance();
const oauth2 = require("../../oauth2/recipe").default.getInstance();
// TODO: dirty hack until we get core support
if (oauth2 !== undefined) {
const oauth2JWKSRes = await fetch("http://localhost:4444/.well-known/jwks.json");
Expand Down
2 changes: 1 addition & 1 deletion lib/build/recipe/oauth2/recipe.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class Recipe extends recipeModule_1.default {
querier_1.Querier.getNewInstanceOrThrowError(recipeId),
this.config,
appInfo,
this.getDefaultIdTokenPayload
this.getDefaultIdTokenPayload.bind(this)
)
);
this.recipeInterfaceImpl = builder.override(this.config.override.functions).build();
Expand Down
9 changes: 9 additions & 0 deletions lib/build/recipe/oauth2client/api/authorisationUrl.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @ts-nocheck
import { APIInterface, APIOptions } from "../";
import { UserContext } from "../../../types";
export default function authorisationUrlAPI(
apiImplementation: APIInterface,
_tenantId: string,
options: APIOptions,
userContext: UserContext
): Promise<boolean>;
44 changes: 44 additions & 0 deletions lib/build/recipe/oauth2client/api/authorisationUrl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use strict";
/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
var __importDefault =
(this && this.__importDefault) ||
function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../../../utils");
const error_1 = __importDefault(require("../../../error"));
async function authorisationUrlAPI(apiImplementation, _tenantId, options, userContext) {
if (apiImplementation.authorisationUrlGET === undefined) {
return false;
}
// TODO: Check if we can rename `redirectURIOnProviderDashboard` to a more suitable name
const redirectURIOnProviderDashboard = options.req.getKeyValueFromQuery("redirectURIOnProviderDashboard");
if (redirectURIOnProviderDashboard === undefined || typeof redirectURIOnProviderDashboard !== "string") {
throw new error_1.default({
type: error_1.default.BAD_INPUT_ERROR,
message: "Please provide the redirectURIOnProviderDashboard as a GET param",
});
}
let result = await apiImplementation.authorisationUrlGET({
redirectURIOnProviderDashboard,
options,
userContext,
});
utils_1.send200Response(options.res, result);
return true;
}
exports.default = authorisationUrlAPI;
3 changes: 3 additions & 0 deletions lib/build/recipe/oauth2client/api/implementation.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @ts-nocheck
import { APIInterface } from "../";
export default function getAPIInterface(): APIInterface;
66 changes: 66 additions & 0 deletions lib/build/recipe/oauth2client/api/implementation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use strict";
var __importDefault =
(this && this.__importDefault) ||
function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const session_1 = __importDefault(require("../../session"));
function getAPIInterface() {
return {
authorisationUrlGET: async function ({ options, redirectURIOnProviderDashboard, userContext }) {
const providerConfig = await options.recipeImplementation.getProviderConfig({ userContext });
const authUrl = await options.recipeImplementation.getAuthorisationRedirectURL({
providerConfig,
redirectURIOnProviderDashboard,
userContext,
});
return Object.assign({ status: "OK" }, authUrl);
},
signInPOST: async function (input) {
const { options, tenantId, userContext } = input;
const providerConfig = await options.recipeImplementation.getProviderConfig({ userContext });
let oAuthTokensToUse = {};
if ("redirectURIInfo" in input && input.redirectURIInfo !== undefined) {
oAuthTokensToUse = await options.recipeImplementation.exchangeAuthCodeForOAuthTokens({
providerConfig,
redirectURIInfo: input.redirectURIInfo,
userContext,
});
} else if ("oAuthTokens" in input && input.oAuthTokens !== undefined) {
oAuthTokensToUse = input.oAuthTokens;
} else {
throw Error("should never come here");
}
const { userId, rawUserInfoFromProvider } = await options.recipeImplementation.getUserInfo({
providerConfig,
oAuthTokens: oAuthTokensToUse,
userContext,
});
const { user, recipeUserId } = await options.recipeImplementation.signIn({
userId,
tenantId,
rawUserInfoFromProvider,
oAuthTokens: oAuthTokensToUse,
userContext,
});
const session = await session_1.default.createNewSession(
options.req,
options.res,
tenantId,
recipeUserId,
undefined,
undefined,
userContext
);
return {
status: "OK",
user,
session,
oAuthTokens: oAuthTokensToUse,
rawUserInfoFromProvider,
};
},
};
}
exports.default = getAPIInterface;
9 changes: 9 additions & 0 deletions lib/build/recipe/oauth2client/api/signin.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @ts-nocheck
import { APIInterface, APIOptions } from "..";
import { UserContext } from "../../../types";
export default function signInAPI(
apiImplementation: APIInterface,
tenantId: string,
options: APIOptions,
userContext: UserContext
): Promise<boolean>;
81 changes: 81 additions & 0 deletions lib/build/recipe/oauth2client/api/signin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use strict";
/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
var __importDefault =
(this && this.__importDefault) ||
function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const error_1 = __importDefault(require("../../../error"));
const utils_1 = require("../../../utils");
const session_1 = __importDefault(require("../../session"));
async function signInAPI(apiImplementation, tenantId, options, userContext) {
if (apiImplementation.signInPOST === undefined) {
return false;
}
const bodyParams = await options.req.getJSONBody();
let redirectURIInfo;
let oAuthTokens;
if (bodyParams.redirectURIInfo !== undefined) {
if (bodyParams.redirectURIInfo.redirectURIOnProviderDashboard === undefined) {
throw new error_1.default({
type: error_1.default.BAD_INPUT_ERROR,
message: "Please provide the redirectURIOnProviderDashboard in request body",
});
}
redirectURIInfo = bodyParams.redirectURIInfo;
} else if (bodyParams.oAuthTokens !== undefined) {
oAuthTokens = bodyParams.oAuthTokens;
} else {
throw new error_1.default({
type: error_1.default.BAD_INPUT_ERROR,
message: "Please provide one of redirectURIInfo or oAuthTokens in the request body",
});
}
let session = await session_1.default.getSession(
options.req,
options.res,
{
sessionRequired: false,
overrideGlobalClaimValidators: () => [],
},
userContext
);
if (session !== undefined) {
tenantId = session.getTenantId();
}
let result = await apiImplementation.signInPOST({
tenantId,
redirectURIInfo,
oAuthTokens,
session,
options,
userContext,
});
if (result.status === "OK") {
utils_1.send200Response(
options.res,
Object.assign(
{ status: result.status },
utils_1.getBackwardsCompatibleUserInfo(options.req, result, userContext)
)
);
} else {
utils_1.send200Response(options.res, result);
}
return true;
}
exports.default = signInAPI;
3 changes: 3 additions & 0 deletions lib/build/recipe/oauth2client/constants.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @ts-nocheck
export declare const AUTHORISATION_API = "/oauth2client/authorisationurl";
export declare const SIGN_IN_API = "/oauth2client/signin";
19 changes: 19 additions & 0 deletions lib/build/recipe/oauth2client/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use strict";
/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SIGN_IN_API = exports.AUTHORISATION_API = void 0;
exports.AUTHORISATION_API = "/oauth2client/authorisationurl";
exports.SIGN_IN_API = "/oauth2client/signin";
30 changes: 30 additions & 0 deletions lib/build/recipe/oauth2client/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// @ts-nocheck
import Recipe from "./recipe";
import { RecipeInterface, APIInterface, APIOptions, OAuthTokens } from "./types";
export default class Wrapper {
static init: typeof Recipe.init;
static getAuthorisationRedirectURL(
redirectURIOnProviderDashboard: string,
userContext?: Record<string, any>
): Promise<{
urlWithQueryParams: string;
pkceCodeVerifier?: string | undefined;
}>;
static exchangeAuthCodeForOAuthTokens(
redirectURIInfo: {
redirectURIOnProviderDashboard: string;
redirectURIQueryParams: any;
pkceCodeVerifier?: string | undefined;
},
userContext?: Record<string, any>
): Promise<import("./types").OAuthTokenResponse>;
static getUserInfo(
oAuthTokens: OAuthTokens,
userContext?: Record<string, any>
): Promise<import("./types").UserInfo>;
}
export declare let init: typeof Recipe.init;
export declare let getAuthorisationRedirectURL: typeof Wrapper.getAuthorisationRedirectURL;
export declare let exchangeAuthCodeForOAuthTokens: typeof Wrapper.exchangeAuthCodeForOAuthTokens;
export declare let getUserInfo: typeof Wrapper.getUserInfo;
export type { RecipeInterface, APIInterface, APIOptions };
65 changes: 65 additions & 0 deletions lib/build/recipe/oauth2client/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use strict";
/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
var __importDefault =
(this && this.__importDefault) ||
function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getUserInfo = exports.exchangeAuthCodeForOAuthTokens = exports.getAuthorisationRedirectURL = exports.init = void 0;
const utils_1 = require("../../utils");
const recipe_1 = __importDefault(require("./recipe"));
class Wrapper {
static async getAuthorisationRedirectURL(redirectURIOnProviderDashboard, userContext) {
const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl;
const providerConfig = await recipeInterfaceImpl.getProviderConfig({
userContext: utils_1.getUserContext(userContext),
});
return await recipeInterfaceImpl.getAuthorisationRedirectURL({
providerConfig,
redirectURIOnProviderDashboard,
userContext: utils_1.getUserContext(userContext),
});
}
static async exchangeAuthCodeForOAuthTokens(redirectURIInfo, userContext) {
const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl;
const providerConfig = await recipeInterfaceImpl.getProviderConfig({
userContext: utils_1.getUserContext(userContext),
});
return await recipeInterfaceImpl.exchangeAuthCodeForOAuthTokens({
providerConfig,
redirectURIInfo,
userContext: utils_1.getUserContext(userContext),
});
}
static async getUserInfo(oAuthTokens, userContext) {
const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl;
const providerConfig = await recipeInterfaceImpl.getProviderConfig({
userContext: utils_1.getUserContext(userContext),
});
return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserInfo({
providerConfig,
oAuthTokens,
userContext: utils_1.getUserContext(userContext),
});
}
}
exports.default = Wrapper;
Wrapper.init = recipe_1.default.init;
exports.init = Wrapper.init;
exports.getAuthorisationRedirectURL = Wrapper.getAuthorisationRedirectURL;
exports.exchangeAuthCodeForOAuthTokens = Wrapper.exchangeAuthCodeForOAuthTokens;
exports.getUserInfo = Wrapper.getUserInfo;
38 changes: 38 additions & 0 deletions lib/build/recipe/oauth2client/recipe.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// @ts-nocheck
import RecipeModule from "../../recipeModule";
import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserContext } from "../../types";
import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types";
import STError from "../../error";
import NormalisedURLPath from "../../normalisedURLPath";
import type { BaseRequest, BaseResponse } from "../../framework";
export default class Recipe extends RecipeModule {
private static instance;
static RECIPE_ID: string;
config: TypeNormalisedInput;
recipeInterfaceImpl: RecipeInterface;
apiImpl: APIInterface;
isInServerlessEnv: boolean;
constructor(
recipeId: string,
appInfo: NormalisedAppinfo,
isInServerlessEnv: boolean,
config: TypeInput,
_recipes: {}
);
static init(config: TypeInput): RecipeListFunction;
static getInstanceOrThrowError(): Recipe;
static reset(): void;
getAPIsHandled: () => APIHandled[];
handleAPIRequest: (
id: string,
tenantId: string,
req: BaseRequest,
res: BaseResponse,
_path: NormalisedURLPath,
_method: HTTPMethod,
userContext: UserContext
) => Promise<boolean>;
handleError: (err: STError, _request: BaseRequest, _response: BaseResponse) => Promise<void>;
getAllCORSHeaders: () => string[];
isErrorFromThisRecipe: (err: any) => err is STError;
}
Loading

0 comments on commit e84eb49

Please sign in to comment.