diff --git a/oauth3.issuer.js b/oauth3.issuer.js new file mode 100644 index 0000000..4ab6611 --- /dev/null +++ b/oauth3.issuer.js @@ -0,0 +1,287 @@ +/* global Promise */ +;(function (exports) { +'use strict'; + +OAUTH3.utils.query.parse = function (search) { + // 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; + } + else { + kvp = arg.split('='); + key = decodeURIComponent(kvp[0]).trim(); + value = decodeURIComponent(kvp[1]).trim(); + argsParsed[key] = value; + } + } + 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; + } + + return { + url: uri + , method: args.method + , data: body + }; + }; + + 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"}}); + } + + //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 } + + , { access_token: "eyJhbGciOiJub25lIn0.eyJleHAiOjE0ODc2MTQyMzUuNzg3LCJzdWIiOiJmYWtlVXNlcklkIn0.fakeSig" + , refresh_token: "eyJhbGciOiJub25lIn0.eyJleHAiOjE0ODc2MTQyMzUuNzg3LCJzdWIiOiJmYWtlVXNlcklkIn0.fakeSig" + , expires_in: "900" + } + )); + } + + //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.authz.redirectWithToken = function (providerUri, session, clientParams, scopes) { + console.info('redirectWithToken scopes'); + console.log(scopes); + + 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 || { + method: 'POST' + , url: 'https://' + directive.provider_url + '/api/org.oauth3.provider/accounts/:accountId' + , bearer: 'Bearer' + }; + 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 + } + } + , logins: [ + { + token: session.access_token + } + ] + }; + + return OAUTH3.request({ + method: dir.method || 'POST' + , url: dir.url + , session: session + , data: data + }); + }; + +}('undefined' !== typeof exports ? exports : window)); \ No newline at end of file