UNPKG

@mvx/identity

Version:

identity is oidc for mvc, type-mvc is base on koa. Decorator, Ioc, AOP mvc framework on server.

543 lines (541 loc) 28.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Resolver = exports.OIDCStrategy = void 0; var tslib_1 = require("tslib"); var ioc_1 = require("@tsdi/ioc"); var components_1 = require("@tsdi/components"); var Strategy_1 = require("./Strategy"); var querystring_1 = require("querystring"); var errors_1 = require("../errors"); var stores_1 = require("../stores"); var url_1 = require("url"); var oauth2_1 = require("./oauth2"); var results_1 = require("./results"); var webfinger = require('webfinger').webfinger; var request = require('request'); /** * OIDC authenticate strategy * * @export * @class OIDCStrategy * @extends {Strategy} * @implements {AfterInit} */ var OIDCStrategy = /** @class */ (function (_super) { tslib_1.__extends(OIDCStrategy, _super); function OIDCStrategy() { return _super !== null && _super.apply(this, arguments) || this; } OIDCStrategy.prototype.onAfterInit = function () { return tslib_1.__awaiter(this, void 0, void 0, function () { return tslib_1.__generator(this, function (_a) { if (!this.name) { this.name = 'openidconnect'; } if (!this.identifierField) { this.identifierField = 'openid_identifier'; } if (!this.sessionKey) { this.sessionKey = ('oauth2:' + url_1.parse(this.authorizationURL).hostname); } if (this.stateStore) { this.stateStore = new stores_1.SessionStore(this.sessionKey); } if (!(this.authorizationURL && this.tokenURL) && this.options.getClient) { throw new Error('OpenID Connect authentication requires getClientCallback option'); } return [2 /*return*/]; }); }); }; OIDCStrategy.prototype.authenticate = function (ctx, options) { return tslib_1.__awaiter(this, void 0, void 0, function () { var state, _a, verifiedResult, meta, verifiedMsg, code, callbackURL, oauth2, accessToken, refreshToken, accessTokenResult, err_1, idToken, idTokenSegments, jwtClaimsStr, jwtClaims_1, missing, iss, sub, load, profile, parsed, userInfoURL, _b, body, res, json, _c, user, info, identifier, idfield, meta_1, callbackURL, parsed_1, params_1, scope, param, state, location, ex_1; var _d; return tslib_1.__generator(this, function (_e) { switch (_e.label) { case 0: options = options || {}; if (ctx.query && ctx.query.error) { if (ctx.query.error === 'access_denied') { return [2 /*return*/, new results_1.FailResult(ctx.query.error_description, 403)]; } else { throw new errors_1.OIDCError(ctx.query.error_description, ctx.query.error, ctx.query.error_uri); } } if (!(ctx.query && ctx.query.code)) return [3 /*break*/, 10]; state = ctx.query.state; return [4 /*yield*/, this.stateStore.verify(ctx, state)]; case 1: _a = _e.sent(), verifiedResult = _a.result, meta = _a.state, verifiedMsg = _a.message; if (!verifiedResult) { return [2 /*return*/, new results_1.FailResult(verifiedMsg, 403)]; } code = ctx.query.code; callbackURL = meta.callbackURL; oauth2 = new oauth2_1.OAuth2(meta.clientID, meta.clientSecret, '', meta.authorizationURL, meta.tokenURL, meta.customHeaders); accessToken = void 0; refreshToken = void 0; accessTokenResult = void 0; _e.label = 2; case 2: _e.trys.push([2, 4, , 5]); return [4 /*yield*/, oauth2.getOAuthAccessToken(code, { grant_type: 'authorization_code', redirect_uri: callbackURL })]; case 3: (_d = _e.sent(), accessToken = _d.accessToken, refreshToken = _d.refreshToken, accessTokenResult = _d.result); return [3 /*break*/, 5]; case 4: err_1 = _e.sent(); throw this.parseOAuthError(err_1); case 5: idToken = accessTokenResult['id_token']; if (!idToken) { throw new Error('ID Token not present in token response'); } idTokenSegments = idToken.split('.'), jwtClaimsStr = void 0; try { jwtClaimsStr = new Buffer(idTokenSegments[1], 'base64').toString(); jwtClaims_1 = JSON.parse(jwtClaimsStr); } catch (ex) { throw ex; } missing = ['iss', 'sub', 'aud', 'exp', 'iat'].filter(function (param) { return !jwtClaims_1[param]; }); if (missing.length) { throw new Error('id token is missing required parameter(s) - ' + missing.join(', ')); } // https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation - check 1. if (jwtClaims_1.iss !== meta.issuer) { throw new Error('id token not issued by correct OpenID provider - ' + 'expected: ' + meta.issuer + ' | from: ' + jwtClaims_1.iss); } // https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation - checks 2 and 3. if (typeof jwtClaims_1.aud === 'string') { if (jwtClaims_1.aud !== meta.clientID) { throw new Error('aud parameter does not include this client - is: ' + jwtClaims_1.aud + '| expected: ' + meta.clientID); } } else if (ioc_1.isArray(jwtClaims_1.aud)) { if (jwtClaims_1.aud.indexOf(meta.clientID) === -1) { throw new Error('aud parameter does not include this client - is: ' + jwtClaims_1.aud + ' | expected to include: ' + meta.clientID); } if (jwtClaims_1.length > 1 && !jwtClaims_1.azp) { throw new Error('azp parameter required with multiple audiences'); } } else { throw new Error('Invalid aud parameter type'); } // https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation - check 4. if (jwtClaims_1.azp && jwtClaims_1.azp !== meta.clientID) { throw new Error('this client is not the authorized party - ' + 'expected: ' + meta.clientID + ' | is: ' + jwtClaims_1.azp); } // Possible TODO: Add accounting for some clock skew. // https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation - check 5. if (jwtClaims_1.exp < (Date.now() / 1000)) { throw new Error('id token has expired'); } // Note: https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation - checks 6 and 7 are out of scope of this library. // https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation - check 8. if (meta.params.max_age && (!jwtClaims_1.auth_time || ((meta.timestamp - meta.params.max_age) > jwtClaims_1.auth_time))) { throw new Error('auth_time in id_token not included or too old'); } if (meta.params.nonce && (!jwtClaims_1.nonce || jwtClaims_1.nonce !== meta.params.nonce)) { throw new Error('Invalid nonce in id_token'); } iss = jwtClaims_1.iss; sub = jwtClaims_1.sub; // Prior to OpenID Connect Basic Client Profile 1.0 - draft 22, the // "sub" claim was named "user_id". Many providers still issue the // claim under the old field, so fallback to that. if (!sub) { sub = jwtClaims_1.user_id; } return [4 /*yield*/, this.shouldLoadUserProfile(iss, sub)]; case 6: load = _e.sent(); profile = undefined; if (!load) return [3 /*break*/, 8]; parsed = url_1.parse(meta.userInfoURL, true); parsed.query['schema'] = 'openid'; delete parsed.search; userInfoURL = url_1.format(parsed); return [4 /*yield*/, oauth2.request('GET', userInfoURL, { 'Authorization': 'Bearer ' + accessToken, 'Accept': 'application/json' }, null, null)]; case 7: _b = _e.sent(), body = _b.result, res = _b.response; profile = {}; try { json = JSON.parse(body); profile.id = json.sub; // Prior to OpenID Connect Basic Client Profile 1.0 - draft 22, the // "sub" key was named "user_id". Many providers still use the old // key, so fallback to that. if (!profile.id) { profile.id = json.user_id; } profile.displayName = json.name; profile.name = { familyName: json.family_name, givenName: json.given_name, middleName: json.middle_name }; profile._raw = body; profile._json = json; } catch (ex) { throw ex; } _e.label = 8; case 8: return [4 /*yield*/, this.verify(ctx, this.passReqToCallback ? iss : undefined, sub, profile, jwtClaims_1, accessToken, refreshToken, accessTokenResult)]; case 9: _c = _e.sent(), user = _c.user, info = _c.info; if (!user) { // TODO, not sure 401 is the correct meaning return [2 /*return*/, new results_1.FailResult(info, 401)]; } return [2 /*return*/, new results_1.SuccessResult(options, user, info)]; case 10: identifier = void 0; idfield = this.identifierField; if (ctx.request.body && ctx.request.body[idfield]) { identifier = ctx.request.body[idfield]; } else if (ctx.query && ctx.query[idfield]) { identifier = ctx.query[idfield]; } return [4 /*yield*/, this.getConfigure(identifier)]; case 11: meta_1 = _e.sent(); callbackURL = options.callbackURL || this.callbackURL; if (callbackURL) { parsed_1 = url_1.parse(callbackURL); if (!parsed_1.protocol) { // The callback URL is relative, resolve a fully qualified URL from the // URL of the originating request. callbackURL = url_1.resolve(ctx.request.origin, callbackURL); } } meta_1.callbackURL = callbackURL; return [4 /*yield*/, this.authorizationParams(options)]; case 12: params_1 = _e.sent(); params_1['response_type'] = 'code'; params_1['client_id'] = meta_1.clientID; if (callbackURL) { params_1.redirect_uri = callbackURL; } scope = options.scope || this.scope; if (ioc_1.isArray(scope)) { scope = scope.join(' '); } if (scope) { params_1.scope = 'openid ' + scope; } else { params_1.scope = 'openid'; } // Optional Parameters ['max_age', 'ui_locals', 'id_token_hint', 'login_hint', 'acr_values'] .filter(function (x) { return x in meta_1; }) .forEach(function (y) { params_1[y] = meta_1[y]; }); if (meta_1.display && ['page', 'popup', 'touch', 'wap'].indexOf(meta_1.display) !== -1) { params_1.display = meta_1.display; } if (meta_1.prompt && ['none', 'login', 'consent', 'select_account'].indexOf(meta_1.prompt) !== -1) { params_1.prompt = meta_1.prompt; } if (meta_1.nonce && typeof meta_1.nonce === 'boolean') { params_1.nonce = stores_1.OIDCUtils.uid(20); } if (meta_1.nonce && typeof meta_1.nonce === 'number') { params_1.nonce = stores_1.OIDCUtils.uid(meta_1.nonce); } if (meta_1.nonce && typeof meta_1.nonce === 'string') { params_1.nonce = meta_1.nonce; } if (params_1.max_age) { meta_1.timestamp = Math.floor(Date.now() / 1000); } meta_1.params = params_1; for (param in params_1) { if (meta_1[param]) { delete meta_1[param]; // Remove redundant information. } } _e.label = 13; case 13: _e.trys.push([13, 15, , 16]); return [4 /*yield*/, this.stateStore.store(ctx, meta_1)]; case 14: state = _e.sent(); params_1.state = state; location = meta_1.authorizationURL + '?' + querystring_1.stringify(params_1); return [2 /*return*/, new results_1.RedirectResult(location)]; case 15: ex_1 = _e.sent(); throw ex_1; case 16: return [2 /*return*/]; } }); }); }; OIDCStrategy.prototype.shouldLoadUserProfile = function (issuer, subject) { return tslib_1.__awaiter(this, void 0, void 0, function () { var _a; return tslib_1.__generator(this, function (_b) { switch (_b.label) { case 0: if (!this.skipUserProfile) return [3 /*break*/, 4]; if (!ioc_1.isFunction(this.skipUserProfile)) return [3 /*break*/, 2]; return [4 /*yield*/, this.skipUserProfile(issuer, subject)]; case 1: _a = _b.sent(); return [3 /*break*/, 3]; case 2: _a = false; _b.label = 3; case 3: return [2 /*return*/, _a]; case 4: return [2 /*return*/, true]; } }); }); }; OIDCStrategy.prototype.parseOAuthError = function (err) { var e; if (err instanceof oauth2_1.OAuth2Error) { try { var json = JSON.parse(err.message); if (json.error) { e = new errors_1.InternalOAuthError("Failed to obtain access token:" + json.error_description, json.error); } } catch (_) { // console.warn('============This error can be ignored=============='); // console.warn(_); // console.warn('==================================================='); } } if (!e) { err.message = "Failed to obtain access token:" + err.message; e = err; } return e; }; OIDCStrategy.prototype.getConfigure = function (identifier) { return tslib_1.__awaiter(this, void 0, void 0, function () { return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: if (!(this.authorizationURL && this.tokenURL)) return [3 /*break*/, 2]; return [4 /*yield*/, this.manualConfigure(identifier)]; case 1: return [2 /*return*/, _a.sent()]; case 2: return [4 /*yield*/, this.dynamicConfigure(identifier)]; case 3: return [2 /*return*/, _a.sent()]; } }); }); }; OIDCStrategy.prototype.dynamicConfigure = function (identifier) { return tslib_1.__awaiter(this, void 0, void 0, function () { var issuer, url, defer; var _this = this; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.injector.getInstance(Resolver).resolve(identifier)]; case 1: issuer = _a.sent(); url = issuer + '/.well-known/openid-configuration'; defer = ioc_1.PromiseUtil.defer(); request.get(url, function (err, res, body) { return tslib_1.__awaiter(_this, void 0, void 0, function () { var config, json, client; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: if (err) { return [2 /*return*/, defer.reject(err)]; } if (res.statusCode !== 200) { return [2 /*return*/, defer.reject(new Error('Unexpected status code from OpenID provider configuration: ' + res.statusCode))]; } config = {}; try { json = JSON.parse(body); config.issuer = json.issuer; config.authorizationURL = json.authorization_endpoint; config.tokenURL = json.token_endpoint; config.userInfoURL = json.userinfo_endpoint; config.registrationURL = json.registration_endpoint; config._raw = json; } catch (ex) { return [2 /*return*/, defer.reject(new Error('Failed to parse OpenID provider configuration'))]; } return [4 /*yield*/, this.options.getClient(config.issuer)]; case 1: client = _a.sent(); config.clientID = client.id; config.clientSecret = client.secret; if (client.redirectURIs) { config.callbackURL = client.redirectURIs[0]; } return [2 /*return*/, defer.resolve(config)]; } }); }); }); return [2 /*return*/, defer.promise]; } }); }); }; OIDCStrategy.prototype.manualConfigure = function (identifier) { return tslib_1.__awaiter(this, void 0, void 0, function () { var missing, params; var _this = this; return tslib_1.__generator(this, function (_a) { missing = ['issuer', 'authorizationURL', 'tokenURL', 'clientID', 'clientSecret'].filter(function (opt) { return !_this.options[opt]; }); if (missing.length) { throw new Error('Manual OpenID configuration is missing required parameter(s) - ' + missing.join(', ')); } params = { issuer: this.issuer, authorizationURL: this.authorizationURL, tokenURL: this.tokenURL, userInfoURL: this.userInfoURL, clientID: this.clientID, clientSecret: this.clientSecret, callbackURL: this.callbackURL }; Object.keys(this.options).map(function (opt) { if (['nonce', 'display', 'prompt', 'max_age', 'ui_locals', 'id_token_hint', 'login_hint', 'acr_values'].indexOf(opt) !== -1) { params[opt] = _this.options[opt]; } }); return [2 /*return*/, params]; }); }); }; OIDCStrategy.ρAnn = function () { return { "name": "OIDCStrategy", "params": { "authenticate": ["ctx", "options"], "shouldLoadUserProfile": ["issuer", "subject"], "parseOAuthError": ["err"], "getConfigure": ["identifier"], "dynamicConfigure": ["identifier"], "manualConfigure": ["identifier"] } }; }; tslib_1.__decorate([ components_1.Input('store'), tslib_1.__metadata("design:type", stores_1.StateStore) ], OIDCStrategy.prototype, "stateStore", void 0); tslib_1.__decorate([ components_1.Input(), tslib_1.__metadata("design:type", Object) ], OIDCStrategy.prototype, "scope", void 0); tslib_1.__decorate([ components_1.Input(), tslib_1.__metadata("design:type", String) ], OIDCStrategy.prototype, "identifierField", void 0); tslib_1.__decorate([ components_1.Input(), tslib_1.__metadata("design:type", String) ], OIDCStrategy.prototype, "issuer", void 0); tslib_1.__decorate([ components_1.Input(), tslib_1.__metadata("design:type", String) ], OIDCStrategy.prototype, "sessionKey", void 0); tslib_1.__decorate([ components_1.Input(), tslib_1.__metadata("design:type", String) ], OIDCStrategy.prototype, "tokenURL", void 0); tslib_1.__decorate([ components_1.Input(), tslib_1.__metadata("design:type", String) ], OIDCStrategy.prototype, "authorizationURL", void 0); tslib_1.__decorate([ components_1.Input(), tslib_1.__metadata("design:type", String) ], OIDCStrategy.prototype, "clientID", void 0); tslib_1.__decorate([ components_1.Input(), tslib_1.__metadata("design:type", String) ], OIDCStrategy.prototype, "clientSecret", void 0); tslib_1.__decorate([ components_1.Input(), tslib_1.__metadata("design:type", String) ], OIDCStrategy.prototype, "callbackURL", void 0); tslib_1.__decorate([ components_1.Input(), tslib_1.__metadata("design:type", String) ], OIDCStrategy.prototype, "userInfoURL", void 0); tslib_1.__decorate([ components_1.Input(), tslib_1.__metadata("design:type", Object) ], OIDCStrategy.prototype, "customHeaders", void 0); tslib_1.__decorate([ components_1.Input(), tslib_1.__metadata("design:type", Function) ], OIDCStrategy.prototype, "verify", void 0); tslib_1.__decorate([ components_1.Input(), tslib_1.__metadata("design:type", String) ], OIDCStrategy.prototype, "passReqToCallback", void 0); tslib_1.__decorate([ components_1.Input(), tslib_1.__metadata("design:type", Object) ], OIDCStrategy.prototype, "skipUserProfile", void 0); tslib_1.__decorate([ components_1.Input(), tslib_1.__metadata("design:type", Function) ], OIDCStrategy.prototype, "authorizationParams", void 0); tslib_1.__decorate([ ioc_1.Inject(components_1.TemplateOptionToken), tslib_1.__metadata("design:type", Object) ], OIDCStrategy.prototype, "options", void 0); tslib_1.__decorate([ ioc_1.Inject(ioc_1.INJECTOR), tslib_1.__metadata("design:type", Object) ], OIDCStrategy.prototype, "injector", void 0); OIDCStrategy = tslib_1.__decorate([ components_1.Component({ selector: 'oidc' }) ], OIDCStrategy); return OIDCStrategy; }(Strategy_1.Strategy)); exports.OIDCStrategy = OIDCStrategy; var REL = 'http://openid.net/specs/connect/1.0/issuer'; var Resolver = /** @class */ (function () { function Resolver() { } Resolver.prototype.resolve = function (identifier) { var defer = ioc_1.PromiseUtil.defer(); webfinger(identifier, REL, { webfingerOnly: true }, function (err, jrd) { if (err) { return defer.reject(err); } if (!jrd.links) { return defer.reject(new errors_1.NoOpenIDError('No links in resource descriptor', jrd)); } var issuer; for (var i = 0; i < jrd.links.length; i++) { var link = jrd.links[i]; if (link.rel === REL) { issuer = link.href; break; } } if (!issuer) { return defer.reject(new errors_1.NoOpenIDError('No OpenID Connect issuer in resource descriptor', jrd)); } return defer.resolve(issuer); }); return defer.promise; }; Resolver.ρAnn = function () { return { "name": "Resolver", "params": { "resolve": ["identifier"] } }; }; Resolver = tslib_1.__decorate([ ioc_1.Singleton() ], Resolver); return Resolver; }()); exports.Resolver = Resolver; //# sourceMappingURL=../sourcemaps/passports/OIDCStrategy.js.map