From bb6bcd826eb9e8901709cc5cd562f99e3eedfd1e Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 20 Feb 2017 15:22:48 -0700 Subject: [PATCH] complete redirect flow --- oauth3.implicit.js | 20 +- oauth3.issuer.js | 764 ++++++++++++++++++++++++++++-------------- oauth3.issuer.mock.js | 81 +++++ 3 files changed, 608 insertions(+), 257 deletions(-) create mode 100644 oauth3.issuer.mock.js diff --git a/oauth3.implicit.js b/oauth3.implicit.js index 6453fb3..87d0c4f 100644 --- a/oauth3.implicit.js +++ b/oauth3.implicit.js @@ -7,11 +7,13 @@ clientUri: function (location) { return OAUTH3.utils.uri.normalize(location.host + location.pathname); } - , _formatError: function (providerUri, params) { - var err = new Error(params.error_description || params.error.message || "Unknown error with provider '" + providerUri + "'"); - err.uri = params.error_uri || params.error.uri; - err.code = params.error.code || params.error; - return err; + , _error: { + parse: function (providerUri, params) { + var err = new Error(params.error_description || params.error.message || "Unknown error with provider '" + providerUri + "'"); + err.uri = params.error_uri || params.error.uri; + err.code = params.error.code || params.error; + return err; + } } , atob: function (base64) { return (exports.atob || require('atob'))(base64); @@ -130,7 +132,7 @@ // 'abc.qrs.xyz' // [ 'abc', 'qrs', 'xyz' ] // [ {}, {}, 'foo' ] - // { header: {}, payload: {}, signature: } + // { header: {}, payload: {}, signature: '' } var parts = str.split(/\./g); var jsons = parts.slice(0, 2).map(function (urlsafe64) { var atob = exports.atob || require('atob'); @@ -676,6 +678,8 @@ return OAUTH3.request({ method: 'GET' , url: OAUTH3.utils.url.normalize(providerUri) + '/.well-known/oauth3/directives.json' + }).then(function (resp) { + return resp.data; }); } @@ -913,8 +917,10 @@ } } }; + OAUTH3.utils._formatError = OAUTH3.utils._error.parse; + if ('undefined' !== typeof Promise) { OAUTH3.PromiseA = Promise; } -}('undefined' !== typeof exports ? exports : window)); +}('undefined' !== typeof exports ? exports : window)); \ No newline at end of file diff --git a/oauth3.issuer.js b/oauth3.issuer.js index 4ab6611..1482fc6 100644 --- a/oauth3.issuer.js +++ b/oauth3.issuer.js @@ -3,285 +3,549 @@ 'use strict'; OAUTH3.utils.query.parse = function (search) { - // parse a query or a hash - if (-1 !== ['#', '?'].indexOf(search[0])) { - search = search.substring(1); + // parse a query or a hash + if (-1 !== ['#', '?'].indexOf(search[0])) { + search = search.substring(1); + } + // Solve for case of search within hash + // example: #/authorization_dialog/?state=...&redirect_uri=... + var queryIndex = search.indexOf('?'); + if (-1 !== queryIndex) { + search = search.substr(queryIndex + 1); + } + + var args = search.split('&'); + var argsParsed = {}; + var i, arg, kvp, key, value; + + for (i = 0; i < args.length; i += 1) { + arg = args[i]; + if (-1 === arg.indexOf('=')) { + argsParsed[decodeURIComponent(arg).trim()] = true; } - // Solve for case of search within hash - // example: #/authorization_dialog/?state=...&redirect_uri=... - var queryIndex = search.indexOf('?'); - if (-1 !== queryIndex) { - search = search.substr(queryIndex + 1); + else { + kvp = arg.split('='); + key = decodeURIComponent(kvp[0]).trim(); + value = decodeURIComponent(kvp[1]).trim(); + argsParsed[key] = value; + } + } + return argsParsed; +}; +OAUTH3.utils.scope.parse = function (scope) { + return (scope||'').split(/[, ]/g); +}; +OAUTH3.utils.url.parse = function (url) { + // TODO browser + // Node should replace this + var parser = document.createElement('a'); + parser.href = url; + return parser; +}; +OAUTH3.utils.url._isRedirectHostSafe = function (referrerUrl, redirectUrl) { + var src = OAUTH3.utils.url.parse(referrerUrl); + var dst = OAUTH3.utils.url.parse(redirectUrl); + + // TODO how should we handle subdomains? + // It should be safe for api.example.com to redirect to example.com + // But it may not be safe for to example.com to redirect to aj.example.com + // It is also probably not safe for sally.example.com to redirect to john.example.com + // The client should have a list of allowed URLs to choose from and perhaps a wildcard will do + // + // api.example.com.evil.com SHOULD NOT match example.com + return dst.hostname === src.hostname; +}; +OAUTH3.utils.url.checkRedirect = function (client, query) { + console.warn("[security] URL path checking not yet implemented"); + + var clientUrl = OAUTH3.utils.url.normalize(client.url); + var redirectUrl = OAUTH3.utils.url.normalize(query.redirect_uri); + + // General rule: + // I can callback to a shorter domain (fewer subs) or a shorter path (on the same domain) + // but not a longer (more subs) or different domain or a longer path (on the same domain) + + + // We can callback to an explicitly listed domain (TODO and path) + if (OAUTH3.utils.url._isRedirectHostSafe(clientUrl, redirectUrl)) { + return true; + } + + return false; +}; +OAUTH3.utils.url.redirect = function (clientParams, grants, tokenOrError) { + // TODO OAUTH3.redirect(clientParams, grants, tokenOrError) + // TODO check redirect safeness right here with grants.client.urls + // TODO check for '#' and '?'. If none, issue warning and use '?' (for backwards compat) + + var authz = { + access_token: tokenOrError.access_token + , token_type: tokenOrError.token_type // 'Bearer' + , refresh_token: tokenOrError.refresh_token + , expires_in: tokenOrError.expires_in // 1800 (but superceded by jwt.exp) + , scope: tokenOrError.scope // superceded by jwt.scp + + , state: clientParams.state + , debug: clientParams.debug + }; + if (tokenOrError.error) { + authz.error = tokenOrError.error.code || tokenOrError.error; + authz.error_description = tokenOrError.error.message || tokenOrError.error_description; + authz.error_uri = tokenOrError.error.uri || tokenOrError.error_uri; + } + var redirect = clientParams.redirect_uri + '#' + window.OAUTH3.utils.query.stringify(authz); + + if (clientParams.debug) { + console.info('final redirect_uri:', redirect); + window.alert("You're in debug mode so we've taken a pause. Hit OK to continue"); + } + + window.location = redirect; +}; + + +OAUTH3.urls.resourceOwnerPassword = function (directive, opts) { + // + // Example Resource Owner Password Request + // (generally for 1st party and direct-partner mobile apps, and webapps) + // + // POST https://example.com/api/org.oauth3.provider/access_token + // { "grant_type": "password", "client_id": "<>", "scope": "<>" + // , "username": "<>", "password": "password" } + // + opts = opts || {}; + var type = 'access_token'; + var grantType = 'password'; + + if (!opts.password) { + if (opts.otp) { + // for backwards compat + opts.password = opts.otp; // 'otp:' + opts.otpUuid + ':' + opts.otp; + } + } + + var scope = opts.scope || directive.authn_scope; + var clientId = opts.appId || opts.clientId || opts.client_id; + var clientAgreeTos = opts.clientAgreeTos || opts.client_agree_tos; + var clientUri = opts.clientUri || opts.client_uri || opts.clientUrl || opts.client_url; + var args = directive[type]; + var otpCode = opts.otp || opts.otpCode || opts.otp_code || opts.otpToken || opts.otp_token || undefined; + var params = { + "grant_type": grantType + , "username": opts.username + , "password": opts.password || otpCode || undefined + , "totp": opts.totp || opts.totpToken || opts.totp_token || undefined + , "otp": otpCode + , "password_type": otpCode && 'otp' + , "otp_code": otpCode + , "otp_uuid": opts.otpUuid || opts.otp_uuid || undefined + , "user_agent": opts.userAgent || opts.useragent || opts.user_agent || undefined // AJ's Macbook + , "jwk": (opts.rememberDevice || opts.remember_device) && opts.jwk || undefined + //, "public_key": opts.rememberDevice && opts.publicKey || undefined + //, "public_key_type": opts.rememberDevice && opts.publicKeyType || undefined // RSA/ECDSA + //, "jwt": opts.jwt // TODO sign a proof with a previously loaded public_key + , debug: opts.debug || undefined + }; + var uri = args.url; + var body; + if (opts.totp) { + params.totp = opts.totp; + } + + if (clientId) { + params.clientId = clientId; + } + if (clientUri) { + params.clientUri = clientUri; + params.clientAgreeTos = clientAgreeTos; + if (!clientAgreeTos) { + throw new Error('Developer Error: missing clientAgreeTos uri'); + } + } + + if (scope) { + params.scope = OAUTH3.utils.scope.stringify(scope); + } + + if ('GET' === args.method.toUpperCase()) { + uri += '?' + OAUTH3.utils.query.stringify(params); + } else { + body = params; + } + + return { + url: uri + , method: args.method + , data: body + }; +}; +OAUTH3.urls.grants = function (directive, opts) { + // directive = { issuer, authorization_decision } + // opts = { response_type, scopes{ granted, requested, pending, accepted } } + + if (!opts) { + throw new Error("You must supply a directive and an options object."); + } + if (!opts.client_id) { + throw new Error("You must supply options.client_id."); + } + if (!opts.session) { + throw new Error("You must supply options.session."); + } + if (!opts.referrer) { + console.warn("You should supply options.referrer"); + } + if (!opts.method) { + console.warn("You must supply options.method as either 'GET', or 'POST'"); + } + if ('POST' === opts.method) { + if ('string' !== typeof opts.scope) { + console.warn("You should supply options.scope as a space-delimited string of scopes"); + } + if (-1 === ['token', 'code'].indexOf(opts.response_type)) { + throw new Error("You must supply options.response_type as 'token' or 'code'"); + } + } + + var url = OAUTH3.utils.url.resolve(directive.issuer, directive.grants.url) + .replace(/(:azp|:client_id)/g, OAUTH3.utils.uri.normalize(opts.client_id || opts.client_uri)) + .replace(/(:sub|:account_id)/g, opts.session.token.sub) + ; + var data = { + client_id: opts.client_id + , client_uri: opts.client_uri + , referrer: opts.referrer + , response_type: opts.response_type + , scope: opts.scope + , tenant_id: opts.tenant_id + }; + var body; + + if ('GET' === opts.method) { + url += '?' + OAUTH3.utils.query.stringify(data); + } + else { + body = data; + } + + return { + method: opts.method + , url: url + , data: body + , session: opts.session + }; +}; + +OAUTH3.authn = {}; + +OAUTH3.authz = {}; +OAUTH3.authz.loginMeta = function (directive, opts) { + if (opts.mock) { + if (opts.mockError) { + return OAUTH3.PromiseA.resolve({data: {error: {message: "Yikes!", code: 'E'}}}); + } + return OAUTH3.PromiseA.resolve({data: {}}); + } + + return OAUTH3.request({ + method: directive.credential_meta.method || 'GET' + // TODO lint urls + , url: OAUTH3.utils.url.resolve(directive.issuer, directive.credential_meta.url) + .replace(':type', 'email') + .replace(':id', opts.email) + }); +}; + +OAUTH3.authz.otp = function (directive, opts) { + if (opts.mock) { + if (opts.mockError) { + return OAUTH3.PromiseA.resolve({data: {error: {message: "Yikes!", code: 'E'}}}); + } + return OAUTH3.PromiseA.resolve({data: {uuid: "uuidblah"}}); + } + + return OAUTH3.request({ + method: directive.credential_otp.url.method || 'POST' + , url: OAUTH3.utils.url.resolve(directive.issuer, directive.credential_otp.url) + , data: { + // TODO replace with signed hosted file + client_agree_tos: 'oauth3.org/tos/draft' + , client_id: directive.issuer // In this case, the issuer is its own client + , client_uri: directive.issuer + , request_otp: true + , username: opts.email + } + }); +}; + +OAUTH3.authz.resourceOwnerPassword = function (directive, opts) { + console.log('ginger bread man'); + var providerUri = directive.issuer; + + //var scope = opts.scope; + //var appId = opts.appId; + return OAUTH3.discover(providerUri, opts).then(function (directive) { + var prequest = OAUTH3.urls.resourceOwnerPassword(directive, opts); + + return OAUTH3.request(prequest).then(function (req) { + var data = req.data; + data.provider_uri = providerUri; + if (data.error) { + return oauth3.PromiseA.reject(OAUTH3.utils._error.parse(providerUri, data.error)); + } + return OAUTH3.hooks.refreshSession( + opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri } + , data + ); + }); + }); +}; +OAUTH3.authz.scopes = function (providerUri, session, clientParams) { + if (clientParams.mock) { + return { + pending: ['oauth3_authn'] // not yet accepted + , granted: [] // all granted, ever + , requested: ['oauth3_authn'] // all requested, now + , accepted: [] // granted (ever) and requested (now) + }; + } + + // OAuth3.requests.grants(providerUri, {}); // return list of grants + // OAuth3.checkGrants(providerUri, {}); // + var clientUri = OAUTH3.utils.uri.normalize(clientParams.client_uri || OAUTH3._browser.window.document.referrer); + var scope = clientParams.scope || ''; + var clientObj = clientParams; + + if (!scope) { + scope = 'oauth3_authn'; + } + + //$('.js-user-avatar').attr('src', userAvatar); + + /* + console.log('grants options'); + console.log(loc.hash); + console.log(loc.search); + console.log(clientObj); + console.log(session.token); + console.log(window.document.referrer); + */ + + return OAUTH3.authz.grants(providerUri, { + method: 'GET' + , client_id: clientUri + , client_uri: clientUri + , session: session + }).then(function (grantResults) { + var grants; + var grantedScopes; + var grantedScopesMap; + var pendingScopes; + var acceptedScopes; + var scopes = scope.split(/[+, ]/g); + var callbackUrl; + + // it doesn't matter who the referrer is as long as the destination + // is an authorized destination for the client in question + // (though it may not hurt to pass the referrer's info on to the client) + if (!OAUTH3.utils.url.checkRedirect(grantResults.client, clientObj)) { + callbackUrl = 'https://oauth3.org/docs/errors#E_REDIRECT_ATTACK' + + '?redirect_uri=' + clientObj.redirect_uri + + '&allowed_urls=' + grantResults.client.url + + '&client_id=' + clientUri + + '&referrer_uri=' + OAUTH3.utils.uri.normalize(window.document.referrer) + ; + if (opts.debug) { + console.log('grantResults Redirect Attack'); + console.log(grantResults); + console.log(clientObj); + window.alert("DEBUG MODE checkRedirect error encountered. Check the console."); + } + location.href = callbackUrl; + return; } - var args = search.split('&'); - var argsParsed = {}; - var i, arg, kvp, key, value; + if ('oauth3_authn' === scope) { + // implicit ppid grant is automatic + console.warn('[security] fix scope checking on backend so that we can do automatic grants'); + // TODO check user preference if implicit ppid grant is allowed + //return generateToken(session, clientObj); + } - for (i = 0; i < args.length; i += 1) { - arg = args[i]; - if (-1 === arg.indexOf('=')) { - argsParsed[decodeURIComponent(arg).trim()] = true; + grants = (grantResults).grants.filter(function (grant) { + if (clientUri === (grant.azp || grant.oauth_client_id || grant.oauthClientId)) { + return true; + } + }); + + grantedScopesMap = {}; + acceptedScopes = []; + pendingScopes = scopes.filter(function (requestedScope) { + return grants.every(function (grant) { + if (!grant.scope) { + grant.scope = 'oauth3_authn'; + } + var gscopes = grant.scope.split(/[+, ]/g); + gscopes.forEach(function (s) { grantedScopesMap[s] = true; }); + if (-1 !== gscopes.indexOf(requestedScope)) { + // already accepted in the past + acceptedScopes.push(requestedScope); } else { - kvp = arg.split('='); - key = decodeURIComponent(kvp[0]).trim(); - value = decodeURIComponent(kvp[1]).trim(); - argsParsed[key] = value; + // true, is pending + return true; } - } - return argsParsed; - }; - - OAUTH3.urls.resourceOwnerPassword = function (directive, opts) { - // - // Example Resource Owner Password Request - // (generally for 1st party and direct-partner mobile apps, and webapps) - // - // POST https://example.com/api/org.oauth3.provider/access_token - // { "grant_type": "password", "client_id": "<>", "scope": "<>" - // , "username": "<>", "password": "password" } - // - opts = opts || {}; - var type = 'access_token'; - var grantType = 'password'; - - if (!opts.password) { - if (opts.otp) { - // for backwards compat - opts.password = opts.otp; // 'otp:' + opts.otpUuid + ':' + opts.otp; - } - } - - var scope = opts.scope || directive.authn_scope; - var clientId = opts.appId || opts.clientId || opts.client_id; - var clientAgreeTos = opts.clientAgreeTos || opts.client_agree_tos; - var clientUri = opts.clientUri || opts.client_uri || opts.clientUrl || opts.client_url; - var args = directive[type]; - var otpCode = opts.otp || opts.otpCode || opts.otp_code || opts.otpToken || opts.otp_token || undefined; - var params = { - "grant_type": grantType - , "username": opts.username - , "password": opts.password || otpCode || undefined - , "totp": opts.totp || opts.totpToken || opts.totp_token || undefined - , "otp": otpCode - , "password_type": otpCode && 'otp' - , "otp_code": otpCode - , "otp_uuid": opts.otpUuid || opts.otp_uuid || undefined - , "user_agent": opts.userAgent || opts.useragent || opts.user_agent || undefined // AJ's Macbook - , "jwk": (opts.rememberDevice || opts.remember_device) && opts.jwk || undefined - //, "public_key": opts.rememberDevice && opts.publicKey || undefined - //, "public_key_type": opts.rememberDevice && opts.publicKeyType || undefined // RSA/ECDSA - //, "jwt": opts.jwt // TODO sign a proof with a previously loaded public_key - , debug: opts.debug || undefined - }; - var uri = args.url; - var body; - if (opts.totp) { - params.totp = opts.totp; - } - - if (clientId) { - params.clientId = clientId; - } - if (clientUri) { - params.clientUri = clientUri; - params.clientAgreeTos = clientAgreeTos; - if (!clientAgreeTos) { - throw new Error('Developer Error: missing clientAgreeTos uri'); - } - } - - if (scope) { - params.scope = core.stringifyscope(scope); - } - - if ('GET' === args.method.toUpperCase()) { - uri += '?' + core.querystringify(params); - } else { - body = params; - } + }); + }); + grantedScopes = Object.keys(grantedScopesMap); return { - url: uri - , method: args.method - , data: body + pending: pendingScopes // not yet accepted + , granted: grantedScopes // all granted, ever + , requested: scopes // all requested, now + , accepted: acceptedScopes // granted (ever) and requested (now) }; - }; + }); +}; +OAUTH3.authz.grants = function (providerUri, opts) { + return OAUTH3.discover(providerUri, { + client_id: providerUri + , debug: opts.debug + }).then(function (directive) { + console.log('providerUri', providerUri); + console.log('directive', directive); - OAUTH3.authz = {}; - OAUTH3.authz.loginMeta = function (directive, opts) { - if (opts.mock) { - if (opts.mockError) { - return OAUTH3.PromiseA.resolve({data: {error: {message: "Yikes!", code: 'E'}}}); - } - return OAUTH3.PromiseA.resolve({data: {}}); - } - - return OAUTH3.request({ - method: directive.credential_meta.method || 'GET' - // TODO lint urls - , url: OAUTH3.utils.url.resolve(directive.issuer, directive.credential_meta.url) - .replace(':type', 'email') - .replace(':id', opts.email) - }); - }; - - OAUTH3.authz.otp = function (directive, opts) { - if (opts.mock) { - if (opts.mockError) { - return OAUTH3.PromiseA.resolve({data: {error: {message: "Yikes!", code: 'E'}}}); - } - return OAUTH3.PromiseA.resolve({data: {uuid: "uuidblah"}}); - } - - return OAUTH3.request({ - method: directive.credential_otp.url.method || 'POST' - , url: OAUTH3.utils.url.resolve(directive.issuer, directive.credential_otp.url) - , data: { - // TODO replace with signed hosted file - client_agree_tos: 'oauth3.org/tos/draft' - , client_id: directive.issuer // In this case, the issuer is its own client - , client_uri: directive.issuer - , request_otp: true - , username: opts.email - } - }); - }; - - OAUTH3.authz.resourceOwnerPassword = function (directive, opts) { - var providerUri = directive.issuer; - if (opts.mock) { - if (opts.mockError) { - return OAUTH3.PromiseA.resolve({data: {error_description: "fake error", error: "errorcode", error_uri: "https://blah"}}); + return OAUTH3.request(OAUTH3.urls.grants(directive, opts), opts).then(function (grantsResult) { + if ('POST' === opts.method) { + // TODO this is clientToken + return grantsResult.originalData || grantsResult.data; } - //core.jwt.encode({header: {alg: 'none'}, payload: {exp: Date.now() / 1000 + 900, sub: 'fakeUserId'}, signature: "fakeSig" }) - - return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session.refresh( - opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri } + var grants = grantsResult.originalData || grantsResult.data; + // TODO + if (grants.error) { + return oauth3.PromiseA.reject(oauth3.utils._formatError(grants.error)); + } - , { access_token: "eyJhbGciOiJub25lIn0.eyJleHAiOjE0ODc2MTQyMzUuNzg3LCJzdWIiOiJmYWtlVXNlcklkIn0.fakeSig" - , refresh_token: "eyJhbGciOiJub25lIn0.eyJleHAiOjE0ODc2MTQyMzUuNzg3LCJzdWIiOiJmYWtlVXNlcklkIn0.fakeSig" - , expires_in: "900" - } - )); - } + console.warn('requests.grants', grants); - //var scope = opts.scope; - //var appId = opts.appId; - return oauth3.discover(providerUri, opts).then(function (directive) { - var prequest = core.urls.resourceOwnerPassword(directive, opts); - - return oauth3.request(prequest).then(function (req) { - var data = (req.originalData || req.data); - data.provider_uri = providerUri; - if (data.error) { - return oauth3.PromiseA.reject(oauth3.core.formatError(providerUri, data.error)); - } - return oauth3.hooks.refreshSession( - opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri } - , data - ); + oauth3.hooks.setGrants(opts.client_id + '-client', grants.client); + grants.grants.forEach(function (grant) { + var clientId = grant.client_id || grant.oauth_client_id || grant.oauthClientId; + // TODO should save as an array + oauth3.hooks.setGrants(clientId, [ grant ]); }); + + return { + client: oauth3.hooks.getGrants(opts.client_id + '-client') + , grants: oauth3.hooks.getGrants(opts.client_id) || [] + }; }); - }; + }); +}; +OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, scopes) { + console.info('redirectWithToken scopes'); + console.log(scopes); - OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, scopes) { - console.info('redirectWithToken scopes'); - console.log(scopes); + scopes.new = scopes.new || []; - scopes.new = scopes.new || []; - - if ('token' === clientParams.response_type) { - // get token and redirect client-side - return OAUTH3.requests.grants(providerUri, { - method: 'POST' - , client_id: clientParams.client_uri - , client_uri: clientParams.client_uri - , scope: scopes.granted.concat(scopes.new).join(',') - , response_type: clientParams.response_type - , referrer: clientParams.referrer - , session: session - , debug: clientParams.debug - }).then(function (results) { - console.info('generate token results'); - console.info(results); - - OAUTH3.redirect(clientParams, scopes, results); - }); - } - else if ('code' === clientParams.response_type) { - // get token and redirect server-side - // (requires insecure form post as per spec) - //OAUTH3.requests.authorizationDecision(); - window.alert("Authorization Code Redirect NOT IMPLEMENTED"); - throw new Error("Authorization Code Redirect NOT IMPLEMENTED"); - } - }; - OAUTH3.requests = {}; - OAUTH3.requests.accounts = {}; - OAUTH3.requests.accounts.update = function (directive, session, opts) { - var dir = directive.update_account || { + if ('token' === clientParams.response_type) { + // get token and redirect client-side + return OAUTH3.authz.grants(providerUri, { method: 'POST' - , url: 'https://' + directive.provider_url + '/api/org.oauth3.provider/accounts/:accountId' - , bearer: 'Bearer' - }; - var url = dir.url - .replace(/:accountId/, opts.accountId) - ; + , client_id: clientParams.client_uri + , client_uri: clientParams.client_uri + , scope: scopes.granted.concat(scopes.new).join(',') + , response_type: clientParams.response_type + , referrer: clientParams.referrer + , session: session + , debug: clientParams.debug + }).then(function (results) { + console.info('generate token results'); + console.info(results); - return OAUTH3.request({ - method: dir.method || 'POST' - , url: url - , headers: { - 'Authorization': (dir.bearer || 'Bearer') + ' ' + session.accessToken - } - , json: { - name: opts.name - , comment: opts.comment - , displayName: opts.displayName - , priority: opts.priority - } + OAUTH3.utils.url.redirect(clientParams, scopes, results); }); + } + else if ('code' === clientParams.response_type) { + // get token and redirect server-side + // (requires insecure form post as per spec) + //OAUTH3.requests.authorizationDecision(); + window.alert("Authorization Code Redirect NOT IMPLEMENTED"); + throw new Error("Authorization Code Redirect NOT IMPLEMENTED"); + } +}; +OAUTH3.requests = {}; +OAUTH3.requests.accounts = {}; +OAUTH3.requests.accounts.update = function (directive, session, opts) { + var dir = directive.update_account || { + method: 'POST' + , url: 'https://' + directive.provider_url + '/api/org.oauth3.provider/accounts/:accountId' + , bearer: 'Bearer' }; - OAUTH3.requests.accounts.create = function (directive, session, account) { - var dir = directive.create_account || { - method: 'POST' - , url: 'https://' + directive.issuer + '/api/org.oauth3.provider/accounts' - , bearer: 'Bearer' - }; - var data = { - // TODO fix the server to just use one scheme - // account = { nick, self: { comment, username } } - // account = { name, comment, display_name, priority } - account: { + var url = dir.url + .replace(/:accountId/, opts.accountId) + ; + + return OAUTH3.request({ + method: dir.method || 'POST' + , url: url + , headers: { + 'Authorization': (dir.bearer || 'Bearer') + ' ' + session.accessToken + } + , json: { + name: opts.name + , comment: opts.comment + , displayName: opts.displayName + , priority: opts.priority + } + }); +}; +OAUTH3.requests.accounts.create = function (directive, session, account) { + var dir = directive.create_account || { + method: 'POST' + , url: 'https://' + directive.issuer + '/api/org.oauth3.provider/accounts' + , bearer: 'Bearer' + }; + var data = { + // TODO fix the server to just use one scheme + // account = { nick, self: { comment, username } } + // account = { name, comment, display_name, priority } + account: { + nick: account.display_name + , name: account.name + , comment: account.comment + , display_name: account.display_name + , priority: account.priority + , self: { nick: account.display_name , name: account.name , comment: account.comment , display_name: account.display_name , priority: account.priority - , self: { - nick: account.display_name - , name: account.name - , comment: account.comment - , display_name: account.display_name - , priority: account.priority - } } - , logins: [ - { - token: session.access_token - } - ] - }; - - return OAUTH3.request({ - method: dir.method || 'POST' - , url: dir.url - , session: session - , data: data - }); + } + , logins: [ + { + token: session.access_token + } + ] }; + return OAUTH3.request({ + method: dir.method || 'POST' + , url: dir.url + , session: session + , data: data + }); +}; + +OAUTH3._browser.isIframe = function isIframe () { + try { + return window.self !== window.top; + } catch (e) { + return true; + } +}; + }('undefined' !== typeof exports ? exports : window)); \ No newline at end of file diff --git a/oauth3.issuer.mock.js b/oauth3.issuer.mock.js new file mode 100644 index 0000000..cb7da28 --- /dev/null +++ b/oauth3.issuer.mock.js @@ -0,0 +1,81 @@ +/* global Promise */ +;(function (exports) { +'use strict'; + + OAUTH3.utils._base64ToUrlSafeBase64 = function (b64) { + // Base64 to URL-safe Base64 + b64 = b64.replace(/\+/g, '-').replace(/\//g, '_'); + b64 = b64.replace(/=+/g, ''); + return b64; + }; + + OAUTH3.jwt.encode = function (parts) { + parts.header = parts.header || { alg: 'none', typ: 'jwt' }; + parts.signature = parts.signature || ''; + + var btoa = exports.btoa || require('btoa'); + var result = [ + OAUTH3.utils._base64ToUrlSafeBase64(btoa(JSON.stringify(parts.header, null))) + , OAUTH3.utils._base64ToUrlSafeBase64(btoa(JSON.stringify(parts.payload, null))) + , parts.signature // should already be url-safe base64 + ].join('.'); + + return result; + }; + + OAUTH3.authn.resourceOwnerPassword = OAUTH3.authz.resourceOwnerPassword = function (directive, opts) { + var providerUri = directive.issuer; + + if (opts.mockError) { + return OAUTH3.PromiseA.resolve({data: {error_description: "fake error", error: "errorcode", error_uri: "https://blah"}}); + } + + return OAUTH3._mockToken(providerUri, opts); + }; + + OAUTH3.authz.grants = function (providerUri, opts) { + if ('POST' === opts.method) { + return OAUTH3._mockToken(providerUri, opts); + } + + return OAUTH3.discover(providerUri, { + client_id: providerUri + , debug: opts.debug + }).then(function (directive) { + return { + client: { + name: "foo" + , client_id: "localhost.foo.daplie.me:8443" + , url: "https://localhost.foo.daplie.me:8443" + } + , grants: [] + }; + }); + }; + + OAUTH3._refreshToken = function (providerUri, opts) { + return OAUTH3._mockToken(providerUri, opts); + }; + + OAUTH3._mockToken = function (providerUri, opts) { + var accessToken = OAUTH3.jwt.encode({ + header: { alg: 'none' } + , payload: { exp: Math.round(Date.now() / 1000) + 900, sub: 'fakeUserId', scp: opts.scope } + , signature: "fakeSig" + }); + + return OAUTH3.hooks.session.refresh( + opts.session || { + provider_uri: providerUri + , client_id: opts.client_id + , client_uri: opts.client_uri || opts.clientUri + } + , { access_token: accessToken + , refresh_token: accessToken + , expires_in: "900" + , scope: opts.scope + } + ); + }; + +}('undefined' !== typeof exports ? exports : window)); \ No newline at end of file