Skip to content

Commit

Permalink
feat: add functions to validate oauth2 tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
porcellus committed Jul 28, 2024
1 parent 92121af commit 4ab2410
Show file tree
Hide file tree
Showing 22 changed files with 326 additions and 97 deletions.
19 changes: 19 additions & 0 deletions lib/build/combinedRemoteJWKSet.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 55 additions & 0 deletions lib/build/combinedRemoteJWKSet.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions lib/build/querier.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/build/recipe/oauth2/recipe.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const recipeImplementation_1 = __importDefault(require("./recipeImplementation")
const utils_1 = require("./utils");
const supertokens_js_override_1 = __importDefault(require("supertokens-js-override"));
const userInfo_1 = __importDefault(require("./api/userInfo"));
const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet");
class Recipe extends recipeModule_1.default {
constructor(recipeId, appInfo, isInServerlessEnv, config) {
super(recipeId, appInfo);
Expand Down Expand Up @@ -118,6 +119,7 @@ class Recipe extends recipeModule_1.default {
if (process.env.TEST_MODE !== "testing") {
throw new Error("calling testing function in non testing env");
}
combinedRemoteJWKSet_1.resetCombinedJWKS();
Recipe.instance = undefined;
}
/* RecipeModule functions */
Expand Down
2 changes: 1 addition & 1 deletion lib/build/recipe/oauth2/recipeImplementation.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RecipeInterface, TypeNormalisedInput, PayloadBuilderFunction, UserInfoB
export default function getRecipeInterface(
querier: Querier,
_config: TypeNormalisedInput,
_appInfo: NormalisedAppinfo,
appInfo: NormalisedAppinfo,
getDefaultIdTokenPayload: PayloadBuilderFunction,
getDefaultUserInfoPayload: UserInfoBuilderFunction
): RecipeInterface;
82 changes: 74 additions & 8 deletions lib/build/recipe/oauth2/recipeImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,56 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
var __createBinding =
(this && this.__createBinding) ||
(Object.create
? function (o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, {
enumerable: true,
get: function () {
return m[k];
},
});
}
: function (o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
});
var __setModuleDefault =
(this && this.__setModuleDefault) ||
(Object.create
? function (o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}
: function (o, v) {
o["default"] = v;
});
var __importStar =
(this && this.__importStar) ||
function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null)
for (var k in mod)
if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault =
(this && this.__importDefault) ||
function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const jose = __importStar(require("jose"));
const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath"));
const querier_1 = require("../../querier");
const utils_1 = require("../../utils");
const OAuth2Client_1 = require("./OAuth2Client");
const __1 = require("../..");
function getRecipeInterface(querier, _config, _appInfo, getDefaultIdTokenPayload, getDefaultUserInfoPayload) {
const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet");
function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload, getDefaultUserInfoPayload) {
return {
getLoginRequest: async function (input) {
const resp = await querier.sendGetRequest(
Expand Down Expand Up @@ -67,7 +105,7 @@ function getRecipeInterface(querier, _config, _appInfo, getDefaultIdTokenPayload
// TODO: FIXME!!!
redirectTo: resp.data.redirect_to.replace(
querier_1.hydraPubDomain,
_appInfo.apiDomain.getAsStringDangerous() + _appInfo.apiBasePath.getAsStringDangerous()
appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous()
),
};
},
Expand All @@ -90,7 +128,7 @@ function getRecipeInterface(querier, _config, _appInfo, getDefaultIdTokenPayload
// TODO: FIXME!!!
redirectTo: resp.data.redirect_to.replace(
querier_1.hydraPubDomain,
_appInfo.apiDomain.getAsStringDangerous() + _appInfo.apiBasePath.getAsStringDangerous()
appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous()
),
};
},
Expand Down Expand Up @@ -137,7 +175,7 @@ function getRecipeInterface(querier, _config, _appInfo, getDefaultIdTokenPayload
// TODO: FIXME!!!
redirectTo: resp.data.redirect_to.replace(
querier_1.hydraPubDomain,
_appInfo.apiDomain.getAsStringDangerous() + _appInfo.apiBasePath.getAsStringDangerous()
appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous()
),
};
},
Expand All @@ -160,7 +198,7 @@ function getRecipeInterface(querier, _config, _appInfo, getDefaultIdTokenPayload
// TODO: FIXME!!!
redirectTo: resp.data.redirect_to.replace(
querier_1.hydraPubDomain,
_appInfo.apiDomain.getAsStringDangerous() + _appInfo.apiBasePath.getAsStringDangerous()
appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous()
),
};
},
Expand Down Expand Up @@ -192,7 +230,7 @@ function getRecipeInterface(querier, _config, _appInfo, getDefaultIdTokenPayload
// TODO: FIXME!!!
redirectTo: resp.data.redirect_to.replace(
querier_1.hydraPubDomain,
_appInfo.apiDomain.getAsStringDangerous() + _appInfo.apiBasePath.getAsStringDangerous()
appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous()
),
};
},
Expand Down Expand Up @@ -276,9 +314,9 @@ function getRecipeInterface(querier, _config, _appInfo, getDefaultIdTokenPayload
};
},
token: async function (input) {
const body = new FormData(); // TODO: we ideally want to avoid using formdata, the core can do the translation
const body = { $isFormData: true }; // TODO: we ideally want to avoid using formdata, the core can do the translation
for (const key in input.body) {
body.append(key, input.body[key]);
body[key] = input.body[key];
}
const res = await querier.sendPostRequest(
new normalisedURLPath_1.default(`/recipe/oauth2/pub/token`),
Expand Down Expand Up @@ -397,6 +435,34 @@ function getRecipeInterface(querier, _config, _appInfo, getDefaultIdTokenPayload
buildUserInfo: async function ({ user, accessTokenPayload, scopes, tenantId, userContext }) {
return getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, userContext);
},
validateOAuth2AccessToken: async function (input) {
const payload = (await jose.jwtVerify(input.token, combinedRemoteJWKSet_1.getCombinedJWKS())).payload;
// TODO: make this configurable?
const expectedIssuer =
appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous();
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) {
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) {
const payload = (await jose.jwtVerify(input.token, combinedRemoteJWKSet_1.getCombinedJWKS())).payload;
// TODO: make this configurable?
const expectedIssuer =
appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous();
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) {
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 };
},
};
}
exports.default = getRecipeInterface;
16 changes: 16 additions & 0 deletions lib/build/recipe/oauth2/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,22 @@ export declare type RecipeInterface = {
errorHint: string;
}
>;
validateOAuth2AccessToken(input: {
token: string;
expectedAudience?: string;
userContext: UserContext;
}): Promise<{
status: "OK";
payload: JSONObject;
}>;
validateOAuth2IdToken(input: {
token: string;
expectedAudience?: string;
userContext: UserContext;
}): Promise<{
status: "OK";
payload: JSONObject;
}>;
buildAccessTokenPayload(input: {
user: User;
session: SessionContainerInterface;
Expand Down
2 changes: 2 additions & 0 deletions lib/build/recipe/session/recipe.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const implementation_1 = __importDefault(require("./api/implementation"));
const supertokens_js_override_1 = __importDefault(require("supertokens-js-override"));
const recipe_1 = __importDefault(require("../openid/recipe"));
const logger_1 = require("../../logger");
const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet");
// For Express
class SessionRecipe extends recipeModule_1.default {
constructor(recipeId, appInfo, isInServerlessEnv, config) {
Expand Down Expand Up @@ -238,6 +239,7 @@ class SessionRecipe extends recipeModule_1.default {
throw new Error("calling testing function in non testing env");
}
SessionRecipe.instance = undefined;
combinedRemoteJWKSet_1.resetCombinedJWKS();
}
}
exports.default = SessionRecipe;
Expand Down
2 changes: 0 additions & 2 deletions lib/build/recipe/session/recipeImplementation.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
// @ts-nocheck
import { JWTVerifyGetKey } from "jose";
import { RecipeInterface, TypeNormalisedInput } from "./types";
import { Querier } from "../../querier";
import { NormalisedAppinfo } from "../../types";
export declare type Helpers = {
querier: Querier;
JWKS: JWTVerifyGetKey;
config: TypeNormalisedInput;
appInfo: NormalisedAppinfo;
getRecipeImpl: () => RecipeInterface;
Expand Down
32 changes: 0 additions & 32 deletions lib/build/recipe/session/recipeImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ var __importDefault =
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const jose_1 = require("jose");
const SessionFunctions = __importStar(require("./sessionFunctions"));
const cookieAndHeaders_1 = require("./cookieAndHeaders");
const utils_1 = require("./utils");
Expand All @@ -55,36 +54,6 @@ const recipeUserId_1 = __importDefault(require("../../recipeUserId"));
const constants_1 = require("../multitenancy/constants");
const constants_2 = require("./constants");
function getRecipeInterface(querier, config, appInfo, getRecipeImplAfterOverrides) {
const JWKS = querier.getAllCoreUrlsForPath("/.well-known/jwks.json").map((url) =>
jose_1.createRemoteJWKSet(new URL(url), {
cooldownDuration: constants_2.JWKCacheCooldownInMs,
cacheMaxAge: constants_2.JWKCacheMaxAgeInMs,
})
);
/**
This function fetches all JWKs from the first available core instance. This combines the other JWKS functions to become
error resistant.
Every core instance a backend is connected to is expected to connect to the same database and use the same key set for
token verification. Otherwise, the result of session verification would depend on which core is currently available.
*/
const combinedJWKS = async (...args) => {
let lastError = undefined;
if (JWKS.length === 0) {
throw Error(
"No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using."
);
}
for (const jwks of JWKS) {
try {
// We await before returning to make sure we catch the error
return await jwks(...args);
} catch (ex) {
lastError = ex;
}
}
throw lastError;
};
let obj = {
createNewSession: async function ({
recipeUserId,
Expand Down Expand Up @@ -436,7 +405,6 @@ function getRecipeInterface(querier, config, appInfo, getRecipeImplAfterOverride
};
let helpers = {
querier,
JWKS: combinedJWKS,
config,
appInfo,
getRecipeImpl: getRecipeImplAfterOverrides,
Expand Down
3 changes: 2 additions & 1 deletion lib/build/recipe/session/sessionFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const logger_1 = require("../../logger");
const recipeUserId_1 = __importDefault(require("../../recipeUserId"));
const constants_1 = require("../multitenancy/constants");
const constants_2 = require("./constants");
const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet");
/**
* @description call this to "login" a user.
*/
Expand Down Expand Up @@ -91,7 +92,7 @@ async function getSession(helpers, parsedAccessToken, antiCsrfToken, doAntiCsrfC
*/
accessTokenInfo = await accessToken_1.getInfoFromAccessToken(
parsedAccessToken,
helpers.JWKS,
combinedRemoteJWKSet_1.getCombinedJWKS(),
helpers.config.antiCsrfFunctionOrString === "VIA_TOKEN" && doAntiCsrfCheck
);
} catch (err) {
Expand Down
Loading

0 comments on commit 4ab2410

Please sign in to comment.