UNPKG

@webda/core

Version:

Expose API with Lambda

312 lines 10.5 kB
import { Counter } from "../core.js"; import { WebdaError } from "../index.js"; import { RegExpStringValidator, Service, ServiceParameters } from "./service.js"; export class OAuthServiceParameters extends ServiceParameters { constructor(params) { super(params); this.scope ?? (this.scope = ["email"]); this.exposeScope ?? (this.exposeScope = false); this.authenticationService ?? (this.authenticationService = "Authentication"); this.defaultHeaders ?? (this.defaultHeaders = { "Cache-Control": "no-cache, no-store, must-revalidate", Pragma: "no-cache", Expires: "0", "X-Frame-Options": "DENY", "X-Content-Type-Options": "nosniff", "X-Robots-Tag": "noindex, nofollow, noarchive", "X-XSS-Protection": "1; mode=block", "Content-Security-Policy": "default-src 'none'; frame-ancestors 'none';" }); } } /** * OAuth service implementing the default OAuth workflow * It is abstract as it does not manage any provider as is */ export class OAuthService extends Service { /** * Ensure default parameter url */ constructor(webda, name, params) { super(webda, name, params); this.parameters.url = this.parameters.url || `${this.getDefaultUrl()}`; } initMetrics() { super.initMetrics(); this.metrics.login = this.getMetric(Counter, { name: "oauth_login", help: "count the number of login", labelNames: ["method"] }); } /** * Load parameters * * @param params */ loadParameters(params) { let result = new OAuthServiceParameters(params); if (result.authorized_uris === undefined) { this.log("WARN", "Not defining authorized_uris is a security risk"); } else { this.authorized_uris = new RegExpStringValidator(result.authorized_uris); } return result; } /** * Allow callback referer to access this url no matter what * @param context * @returns */ async checkRequest(context) { // Only authorize url from this service if (!context.getHttpContext().getRelativeUri().startsWith(this.parameters.url)) { return false; } let regexps = this.getCallbackReferer(); let valid = false; let referer = context.getHttpContext().getUniqueHeader("referer", ""); if (this.parameters.no_referer && referer === "") { return true; } for (let i in regexps) { valid = referer.match(regexps[i]) !== null; if (valid) { break; } } return valid; } /** * Resolve dynamic dependancy */ resolve() { super.resolve(); this._authenticationService = this.getService(this.parameters.authenticationService); return this; } /** * Get OAuth callback query parameters * @returns */ getCallbackQueryParams() { return [ { name: "code", required: true }, { name: "scope", required: true }, { name: "state", required: true } ]; } /** * Add routes for the authentication */ initRoutes() { super.initRoutes(); let name = this.getName(); this.addRoute(`${this.parameters.url}{?redirect?}`, ["GET"], this._redirect, { get: { description: `Log with a ${name} account`, summary: `Redirect to ${name}`, operationId: `logInWith${name}`, responses: { "302": { description: "" }, "400": { description: "Missing token" } } } }); this.addRoute(this.parameters.url + "/callback{?" + this.getCallbackQueryParams() .map(param => param.name + (param.required ? "" : "?")) .join(",") + "}", ["GET"], this._callback, { get: { description: `Get result from ${name} Authentication`, summary: `Use the token provide to validate with ${name} the user`, operationId: `callbackFrom${name}`, responses: { "204": { description: "" }, "400": { description: "Missing token" } } } }); if (this.hasToken()) { this.addRoute(this.parameters.url + "/token", ["POST"], this._token, { post: { description: `Log with a ${name} token`, summary: `Use the token provide to validate with ${name} the user`, operationId: `verify${name}Token`, responses: { "204": { description: "" }, "400": { description: "Missing token" } } } }); } if (this.parameters.exposeScope) { this.addRoute(this.parameters.url + "/scope", ["GET"], this._scope, { get: { description: `List ${name} auth scope for this apps`, summary: "Retrieve the scope intended to be used with this auth", operationId: `get${name}Scope`, responses: { "204": { description: "" }, "400": { description: "Missing token" } } } }); } } /** * Expose the scope used by the authentication * @param ctx */ _scope(ctx) { ctx.write(this.parameters.scope); } /** * Define if this provider allow authentication by tokens * @returns */ hasToken() { return false; } /** * Check if the url is authorized * @param redirect * @param context * @returns */ isAuthorizedUri(redirect, context) { return (!this.parameters.authorized_uris || // If no authorized_uris defined, allow all this.authorized_uris?.validate(redirect) || // If redirect is included in authorized_uris (this.parameters.no_referer && !context.getHttpContext().getUniqueHeader("referer"))); // If no_referer is allowed } /** * Redirect to the OAuth provider * * The calling url must be and authorized_uris if defined * @param ctx */ _redirect(ctx) { // implement default behavior let redirect_uri = this.parameters.redirect_uri || `${ctx.getHttpContext().getAbsoluteUrl()}/callback`; let redirect = ctx.getParameters().redirect || ctx.getHttpContext().getHeaders().referer; if (!this.isAuthorizedUri(redirect, ctx)) { throw new WebdaError.Unauthorized("Unauthorized redirect parameter"); } const session = ctx.getSession(); session.oauth ?? (session.oauth = {}); // Generate 2 random uuid: nonce and state session.oauth.state = this.getWebda().getUuid("base64"); // Redirect to the calling uri session.oauth.redirect = redirect; ctx.redirect(this.generateAuthUrl(redirect_uri, session.oauth.state, ctx)); } /** * Handle a token return * * This is private to avoid any override * @param context */ async _token(context) { const res = await this.handleToken(context); this.addDefaultHeaders(context); await this.handleReturn(context, res.identId, res.profile); await this.emitSync("OAuth.Callback", { ...res, type: "token", provider: this.getName(), context }); this.metrics.login.inc({ method: "token" }); } /** * Add default headers to the response * @param ctx */ addDefaultHeaders(ctx) { Object.entries(this.parameters.defaultHeaders).forEach(([key, value]) => { ctx.setHeader(key, value); }); } /** * Handle a standard url callback * * This is private to avoid any override * @param ctx */ async _callback(ctx) { const res = await this.handleCallback(ctx); this.addDefaultHeaders(ctx); await this.handleReturn(ctx, res.identId, res.profile); await this.emitSync("OAuth.Callback", { ...res, type: "callback", provider: this.getName(), context: ctx }); this.metrics.login.inc({ method: "callback" }); } /** * Once approved by the OAuth provider this will do the common task * @param ctx * @param identId * @param profile */ async handleReturn(ctx, identId, profile, _tokens = undefined) { // If no identId has been provided error if (!identId) { throw new WebdaError.Forbidden("No identId provided by the OAuth provider"); } const session = ctx.getSession(); session.oauth ?? (session.oauth = {}); // If authentication service then create a User/Ident couple if (this._authenticationService) { // Should call the onIdentLogin() await this._authenticationService.onIdentLogin(ctx, this.getName().toLowerCase(), identId, profile); } else { // Login in session ctx.getSession().login(identId, identId); // Store the profile retrieved session.oauth.profile = profile; } // Redirect to our targets if (session.oauth.redirect) { ctx.redirect(session.oauth.redirect); } else { ctx.write("Your authentication is successful"); } // Clean variables from session session.oauth.state = undefined; session.oauth.redirect = undefined; } } //# sourceMappingURL=oauth.js.map