@webda/core
Version:
Expose API with Lambda
312 lines • 10.5 kB
JavaScript
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