diff --git a/lib/build/querier.js b/lib/build/querier.js index 381491527..f156a8a40 100644 --- a/lib/build/querier.js +++ b/lib/build/querier.js @@ -97,7 +97,9 @@ class Querier { let headers = { "cdi-version": apiVersion, }; - if (!isForm) { + if (isForm) { + headers["content-type"] = "application/x-www-form-urlencoded"; + } else { headers["content-type"] = "application/json; charset=utf-8"; } if (Querier.apiKey !== undefined) { diff --git a/lib/build/recipe/oauth2client/recipeImplementation.js b/lib/build/recipe/oauth2client/recipeImplementation.js index 012749192..3ea769a72 100644 --- a/lib/build/recipe/oauth2client/recipeImplementation.js +++ b/lib/build/recipe/oauth2client/recipeImplementation.js @@ -106,6 +106,7 @@ function getRecipeImplementation(_querier, config) { return tokenResponse.jsonResponse; }, getUserInfo: async function ({ providerConfig, oAuthTokens }) { + var _a, _b; let jwks; const accessToken = oAuthTokens["access_token"]; const idToken = oAuthTokens["id_token"]; @@ -146,9 +147,15 @@ function getRecipeImplementation(_querier, config) { rawUserInfoFromProvider.fromUserInfoAPI = userInfoFromAccessToken.jsonResponse; } let userId = undefined; - if (rawUserInfoFromProvider.fromIdTokenPayload !== undefined) { + if ( + ((_a = rawUserInfoFromProvider.fromIdTokenPayload) === null || _a === void 0 ? void 0 : _a.sub) !== + undefined + ) { userId = rawUserInfoFromProvider.fromIdTokenPayload["sub"]; - } else if (rawUserInfoFromProvider.fromUserInfoAPI !== undefined) { + } else if ( + ((_b = rawUserInfoFromProvider.fromUserInfoAPI) === null || _b === void 0 ? void 0 : _b.sub) !== + undefined + ) { userId = rawUserInfoFromProvider.fromUserInfoAPI["sub"]; } if (userId === undefined) { diff --git a/lib/build/recipe/oauth2provider/api/implementation.js b/lib/build/recipe/oauth2provider/api/implementation.js index 4a5f9df54..86d5403ad 100644 --- a/lib/build/recipe/oauth2provider/api/implementation.js +++ b/lib/build/recipe/oauth2provider/api/implementation.js @@ -81,17 +81,13 @@ function getAPIImplementation() { }; }, userInfoGET: async ({ accessTokenPayload, user, scopes, tenantId, options, userContext }) => { - const userInfo = await options.recipeImplementation.buildUserInfo({ + return options.recipeImplementation.buildUserInfo({ user, accessTokenPayload, scopes, tenantId, userContext, }); - return { - status: "OK", - info: userInfo, - }; }, }; } diff --git a/lib/build/recipe/oauth2provider/api/userInfo.js b/lib/build/recipe/oauth2provider/api/userInfo.js index b40716527..cabec7ef8 100644 --- a/lib/build/recipe/oauth2provider/api/userInfo.js +++ b/lib/build/recipe/oauth2provider/api/userInfo.js @@ -36,13 +36,14 @@ async function userInfoGET(apiImplementation, tenantId, options, userContext) { const accessToken = authHeader.replace(/^Bearer /, "").trim(); let accessTokenPayload; try { - accessTokenPayload = await recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.validateOAuth2AccessToken({ - token: accessToken, - // TODO: expectedAudience? - userContext, - }); + const { + payload, + } = await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.validateOAuth2AccessToken({ + token: accessToken, + // TODO: expectedAudience? + userContext, + }); + accessTokenPayload = payload; } catch (error) { options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); utils_1.sendNon200ResponseWithMessage(options.res, "Invalid or expired OAuth2 access token", 400); @@ -52,7 +53,7 @@ async function userInfoGET(apiImplementation, tenantId, options, userContext) { accessTokenPayload === null || typeof accessTokenPayload !== "object" || typeof accessTokenPayload.sub !== "string" || - typeof accessTokenPayload.scope !== "string" + !Array.isArray(accessTokenPayload.scp) ) { options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); utils_1.sendNon200ResponseWithMessage(options.res, "Malformed access token payload", 400); @@ -73,7 +74,7 @@ async function userInfoGET(apiImplementation, tenantId, options, userContext) { accessTokenPayload, user, tenantId, - scopes: accessTokenPayload.scope.split(" "), + scopes: accessTokenPayload.scp, options, userContext, }); diff --git a/lib/build/recipe/oauth2provider/recipeImplementation.js b/lib/build/recipe/oauth2provider/recipeImplementation.js index 5b95b9bc1..720533812 100644 --- a/lib/build/recipe/oauth2provider/recipeImplementation.js +++ b/lib/build/recipe/oauth2provider/recipeImplementation.js @@ -62,6 +62,15 @@ const utils_1 = require("../../utils"); const OAuth2Client_1 = require("./OAuth2Client"); const __1 = require("../.."); const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet"); +// TODO: Remove this core changes are done +function getUpdatedRedirectTo(appInfo, redirectTo) { + return redirectTo + .replace( + querier_1.hydraPubDomain, + appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous() + ) + .replace("oauth2", "oauth2provider"); +} function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload, getDefaultUserInfoPayload) { return { getLoginRequest: async function (input) { @@ -103,10 +112,7 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload, ); return { // TODO: FIXME!!! - redirectTo: resp.data.redirect_to.replace( - querier_1.hydraPubDomain, - appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous() - ), + redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to), }; }, rejectLoginRequest: async function (input) { @@ -126,10 +132,7 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload, ); return { // TODO: FIXME!!! - redirectTo: resp.data.redirect_to.replace( - querier_1.hydraPubDomain, - appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous() - ), + redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to), }; }, getConsentRequest: async function (input) { @@ -172,10 +175,7 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload, ); return { // TODO: FIXME!!! - redirectTo: resp.data.redirect_to.replace( - querier_1.hydraPubDomain, - appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous() - ), + redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to), }; }, rejectConsentRequest: async function (input) { @@ -195,10 +195,7 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload, ); return { // TODO: FIXME!!! - redirectTo: resp.data.redirect_to.replace( - querier_1.hydraPubDomain, - appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous() - ), + redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to), }; }, authorization: async function (input) { @@ -237,7 +234,7 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload, scopes: consentRequest.requestedScope || [], userContext: input.userContext, }); - const accessTokenPayload = this.buildAccessTokenPayload({ + const accessTokenPayload = await this.buildAccessTokenPayload({ user, session: input.session, defaultPayload: Object.assign( @@ -251,6 +248,7 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload, : _b.join(" ")) !== null && _c !== void 0 ? _c : "", + aud: [consentRequest.client.clientId], } ), userContext: input.userContext, @@ -405,6 +403,7 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload, return getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, userContext); }, validateOAuth2AccessToken: async function (input) { + var _a, _b, _c, _d; const payload = (await jose.jwtVerify(input.token, combinedRemoteJWKSet_1.getCombinedJWKS())).payload; // TODO: make this configurable? // const expectedIssuer = @@ -412,17 +411,24 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload, // if (payload.iss !== expectedIssuer) { // throw new Error("Issuer mismatch: this token was likely issued by another application or spoofed"); // } - if ( - input.expectedAudience !== undefined && - payload.aud !== input.expectedAudience && - !(payload.aud instanceof Array && payload.aud.includes(input.expectedAudience)) - ) { + // TODO: Fix this + const aud = + (_b = (_a = payload.ext) === null || _a === void 0 ? void 0 : _a.aud) !== null && _b !== void 0 + ? _b + : payload.aud instanceof Array + ? payload.aud + : (_d = (_c = payload.aud) === null || _c === void 0 ? void 0 : _c.split(" ")) !== null && + _d !== void 0 + ? _d + : []; + if (input.expectedAudience !== undefined && !aud.includes(input.expectedAudience)) { throw new Error("Audience mismatch: this token doesn't belong to the specified client"); } // TODO: add a check to make sure this is the right token type as they can be signed with the same key return { status: "OK", payload: payload }; }, validateOAuth2IdToken: async function (input) { + var _a, _b; const payload = (await jose.jwtVerify(input.token, combinedRemoteJWKSet_1.getCombinedJWKS())).payload; // TODO: make this configurable? // const expectedIssuer = @@ -430,11 +436,14 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload, // if (input.expectedAudience !== undefined && payload.iss !== expectedIssuer) { // throw new Error("Issuer mismatch: this token was likely issued by another application or spoofed"); // } - if ( - input.expectedAudience !== undefined && - payload.aud !== input.expectedAudience && - !(payload.aud instanceof Array && payload.aud.includes(input.expectedAudience)) - ) { + const aud = + payload.aud instanceof Array + ? payload.aud + : (_b = (_a = payload.aud) === null || _a === void 0 ? void 0 : _a.split(" ")) !== null && + _b !== void 0 + ? _b + : []; + if (input.expectedAudience !== undefined && !aud.includes(input.expectedAudience)) { throw new Error("Audience mismatch: this token doesn't belong to the specified client"); } // TODO: add a check to make sure this is the right token type as they can be signed with the same key diff --git a/lib/build/recipe/oauth2provider/types.d.ts b/lib/build/recipe/oauth2provider/types.d.ts index 8b10f63a2..e5e56e94d 100644 --- a/lib/build/recipe/oauth2provider/types.d.ts +++ b/lib/build/recipe/oauth2provider/types.d.ts @@ -308,13 +308,7 @@ export declare type APIInterface = { tenantId: string; options: APIOptions; userContext: UserContext; - }) => Promise< - | { - status: "OK"; - info: JSONObject; - } - | GeneralErrorResponse - >); + }) => Promise); }; export declare type OAuth2ClientOptions = { clientId: string; diff --git a/lib/ts/querier.ts b/lib/ts/querier.ts index 7832e443a..0ad82f47b 100644 --- a/lib/ts/querier.ts +++ b/lib/ts/querier.ts @@ -144,7 +144,9 @@ export class Querier { let headers: any = { "cdi-version": apiVersion, }; - if (!isForm) { + if (isForm) { + headers["content-type"] = "application/x-www-form-urlencoded"; + } else { headers["content-type"] = "application/json; charset=utf-8"; } if (Querier.apiKey !== undefined) { diff --git a/lib/ts/recipe/oauth2client/recipeImplementation.ts b/lib/ts/recipe/oauth2client/recipeImplementation.ts index 68748247b..11b277965 100644 --- a/lib/ts/recipe/oauth2client/recipeImplementation.ts +++ b/lib/ts/recipe/oauth2client/recipeImplementation.ts @@ -201,9 +201,9 @@ export default function getRecipeImplementation(_querier: Querier, config: TypeN let userId: string | undefined = undefined; - if (rawUserInfoFromProvider.fromIdTokenPayload !== undefined) { + if (rawUserInfoFromProvider.fromIdTokenPayload?.sub !== undefined) { userId = rawUserInfoFromProvider.fromIdTokenPayload["sub"]; - } else if (rawUserInfoFromProvider.fromUserInfoAPI !== undefined) { + } else if (rawUserInfoFromProvider.fromUserInfoAPI?.sub !== undefined) { userId = rawUserInfoFromProvider.fromUserInfoAPI["sub"]; } diff --git a/lib/ts/recipe/oauth2provider/api/implementation.ts b/lib/ts/recipe/oauth2provider/api/implementation.ts index 47eb72f24..a3acfdfe1 100644 --- a/lib/ts/recipe/oauth2provider/api/implementation.ts +++ b/lib/ts/recipe/oauth2provider/api/implementation.ts @@ -85,18 +85,13 @@ export default function getAPIImplementation(): APIInterface { }; }, userInfoGET: async ({ accessTokenPayload, user, scopes, tenantId, options, userContext }) => { - const userInfo = await options.recipeImplementation.buildUserInfo({ + return options.recipeImplementation.buildUserInfo({ user, accessTokenPayload, scopes, tenantId, userContext, }); - - return { - status: "OK", - info: userInfo, - }; }, }; } diff --git a/lib/ts/recipe/oauth2provider/api/userInfo.ts b/lib/ts/recipe/oauth2provider/api/userInfo.ts index 6a7a07828..4c9ff043c 100644 --- a/lib/ts/recipe/oauth2provider/api/userInfo.ts +++ b/lib/ts/recipe/oauth2provider/api/userInfo.ts @@ -43,13 +43,14 @@ export default async function userInfoGET( let accessTokenPayload: JSONObject; try { - accessTokenPayload = await OAuth2ProviderRecipe.getInstanceOrThrowError().recipeInterfaceImpl.validateOAuth2AccessToken( - { - token: accessToken, - // TODO: expectedAudience? - userContext, - } - ); + const { + payload, + } = await OAuth2ProviderRecipe.getInstanceOrThrowError().recipeInterfaceImpl.validateOAuth2AccessToken({ + token: accessToken, + // TODO: expectedAudience? + userContext, + }); + accessTokenPayload = payload; } catch (error) { options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); sendNon200ResponseWithMessage(options.res, "Invalid or expired OAuth2 access token", 400); @@ -60,7 +61,7 @@ export default async function userInfoGET( accessTokenPayload === null || typeof accessTokenPayload !== "object" || typeof accessTokenPayload.sub !== "string" || - typeof accessTokenPayload.scope !== "string" + !Array.isArray(accessTokenPayload.scp) ) { options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); sendNon200ResponseWithMessage(options.res, "Malformed access token payload", 400); @@ -81,7 +82,7 @@ export default async function userInfoGET( accessTokenPayload, user, tenantId, - scopes: accessTokenPayload.scope.split(" "), + scopes: accessTokenPayload.scp as string[], options, userContext, }); diff --git a/lib/ts/recipe/oauth2provider/recipeImplementation.ts b/lib/ts/recipe/oauth2provider/recipeImplementation.ts index 269aa2f70..9c6366b99 100644 --- a/lib/ts/recipe/oauth2provider/recipeImplementation.ts +++ b/lib/ts/recipe/oauth2provider/recipeImplementation.ts @@ -31,6 +31,13 @@ import { getUser } from "../.."; import { getCombinedJWKS } from "../../combinedRemoteJWKSet"; import { JSONObject } from "@loopback/core"; +// TODO: Remove this core changes are done +function getUpdatedRedirectTo(appInfo: NormalisedAppinfo, redirectTo: string) { + return redirectTo + .replace(hydraPubDomain, appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous()) + .replace("oauth2", "oauth2provider"); +} + export default function getRecipeInterface( querier: Querier, _config: TypeNormalisedInput, @@ -80,10 +87,7 @@ export default function getRecipeInterface( return { // TODO: FIXME!!! - redirectTo: resp.data.redirect_to.replace( - hydraPubDomain, - appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous() - ), + redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to), }; }, rejectLoginRequest: async function (this: RecipeInterface, input): Promise<{ redirectTo: string }> { @@ -104,10 +108,7 @@ export default function getRecipeInterface( return { // TODO: FIXME!!! - redirectTo: resp.data.redirect_to.replace( - hydraPubDomain, - appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous() - ), + redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to), }; }, getConsentRequest: async function (this: RecipeInterface, input): Promise { @@ -152,10 +153,7 @@ export default function getRecipeInterface( return { // TODO: FIXME!!! - redirectTo: resp.data.redirect_to.replace( - hydraPubDomain, - appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous() - ), + redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to), }; }, @@ -177,10 +175,7 @@ export default function getRecipeInterface( return { // TODO: FIXME!!! - redirectTo: resp.data.redirect_to.replace( - hydraPubDomain, - appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous() - ), + redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to), }; }, authorization: async function (this: RecipeInterface, input) { diff --git a/lib/ts/recipe/oauth2provider/types.ts b/lib/ts/recipe/oauth2provider/types.ts index fdfb4e036..dbe02ba1d 100644 --- a/lib/ts/recipe/oauth2provider/types.ts +++ b/lib/ts/recipe/oauth2provider/types.ts @@ -403,7 +403,7 @@ export type APIInterface = { tenantId: string; options: APIOptions; userContext: UserContext; - }) => Promise<{ status: "OK"; info: JSONObject } | GeneralErrorResponse>); + }) => Promise); }; export type OAuth2ClientOptions = {