@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
JavaScript
"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