@bitkidd/adonis-ally-apple
Version:
An Apple driver for Adonis.js Ally
207 lines (206 loc) • 7.64 kB
JavaScript
"use strict";
/*
|--------------------------------------------------------------------------
| Apple Ally Oauth driver
|--------------------------------------------------------------------------
|
| This is a Ally Oauth Driver for Apple
|
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppleDriver = void 0;
const standalone_1 = require("@adonisjs/ally/build/standalone");
const jwks_rsa_1 = __importDefault(require("jwks-rsa"));
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
/**
* Apple Driver implementation
*/
class AppleDriver extends standalone_1.Oauth2Driver {
constructor(ctx, config) {
super(ctx, config);
this.config = config;
/**
* The URL for the redirect request. The user will be redirected on this page
* to authorize the request.
*/
this.authorizeUrl = 'https://appleid.apple.com/auth/authorize';
/**
* The URL to hit to exchange the authorization code for the access token
*/
this.accessTokenUrl = 'https://appleid.apple.com/auth/token';
/**
* JWKS Client, it is used in Apple key verification process
*/
this.jwksClient = null;
/**
* The param name for the authorization code. Read the documentation of your oauth
* provider and update the param name to match the query string field name in
* which the oauth provider sends the authorization_code post redirect.
*/
this.codeParamName = 'code';
/**
* The param name for the error. Read the documentation of your oauth provider and update
* the param name to match the query string field name in which the oauth provider sends
* the error post redirect
*/
this.errorParamName = 'error';
/**
* Cookie name for storing the CSRF token. Make sure it is always unique. So a better
* approach is to prefix the oauth provider name to `oauth_state` value. For example:
* For example: "facebook_oauth_state"
*/
this.stateCookieName = 'apple_oauth_state';
/**
* Parameter name to be used for sending and receiving the state from.
* Read the documentation of your oauth provider and update the param
* name to match the query string used by the provider for exchanging
* the state.
*/
this.stateParamName = 'state';
/**
* Parameter name for sending the scopes to the oauth provider.
*/
this.scopeParamName = 'scope';
/**
* The separator indentifier for defining multiple scopes
*/
this.scopesSeparator = ' ';
/**
* Initiate JWKS client
*/
this.jwksClient = (0, jwks_rsa_1.default)({
rateLimit: true,
cache: true,
cacheMaxEntries: 100,
cacheMaxAge: 1000 * 60 * 60 * 24,
jwksUri: 'https://appleid.apple.com/auth/keys',
});
/**
* Extremely important to call the following method to clear the
* state set by the redirect request.
*/
this.loadState();
}
/**
* Optionally configure the authorization redirect request. The actual request
* is made by the base implementation of "Oauth2" driver and this is a
* hook to pre-configure the request.
*/
configureRedirectRequest(request) {
/**
* Define user defined scopes or the default one's
*/
request.scopes(this.config.scopes || ['email']);
request.param('client_id', this.config.appId);
request.param('response_type', 'code');
request.param('response_mode', 'form_post');
request.param('grant_type', 'authorization_code');
}
/**
* Update the implementation to tell if the error received during redirect
* means "ACCESS DENIED".
*/
accessDenied() {
return this.ctx.request.input('error') === 'user_denied';
}
/**
* Get Apple Signning Keys to verify token
* @param token an id_token receoived from Apple
* @returns signing key
*/
async getAppleSigningKey(token) {
const decodedToken = jsonwebtoken_1.default.decode(token, { complete: true });
const key = await this.jwksClient?.getSigningKey(decodedToken?.header.kid);
return key?.publicKey || key?.rsaPublicKey;
}
/**
* Generates Client Secret
* https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
* @returns clientSecret
*/
generateClientSecret() {
const clientSecret = jsonwebtoken_1.default.sign({}, this.config.clientSecret, {
algorithm: 'ES256',
keyid: this.config.clientId,
issuer: this.config.teamId,
audience: 'https://appleid.apple.com',
subject: this.config.appId,
expiresIn: 60,
header: { alg: 'ES256', kid: this.config.clientId },
});
return clientSecret;
}
/**
* Parses user info from the Apple Token
*/
async getUserInfo(token) {
const signingKey = await this.getAppleSigningKey(token);
const decodedUser = jsonwebtoken_1.default.verify(token, signingKey, {
issuer: 'https://appleid.apple.com',
audience: this.config.appId,
});
const firstName = decodedUser?.user?.name?.firstName || '';
const lastName = decodedUser?.user?.name?.lastName || '';
return {
id: decodedUser.sub,
avatarUrl: null,
original: null,
nickName: decodedUser.sub,
name: `${firstName}${lastName ? ` ${lastName}` : ''}`,
email: decodedUser.email,
emailVerificationState: decodedUser.email_verified === 'true' ? 'verified' : 'unverified',
};
}
/**
* Get access token
*/
async accessToken(callback) {
/**
* We expect the user to handle errors before calling this method
*/
if (this.hasError()) {
throw standalone_1.OauthException.missingAuthorizationCode(this.codeParamName);
}
/**
* We expect the user to properly handle the state mis-match use case before
* calling this method
*/
if (this.stateMisMatch()) {
throw standalone_1.OauthException.stateMisMatch();
}
return this.getAccessToken((request) => {
request.header('Content-Type', 'application/x-www-form-urlencoded');
request.field('client_id', this.config.appId);
request.field('client_secret', this.generateClientSecret());
request.field(this.codeParamName, this.getCode());
if (typeof callback === 'function') {
callback(request);
}
});
}
/**
* Returns details for the authorized user
*/
async user(callback) {
const token = await this.accessToken(callback);
const user = await this.getUserInfo(token.id_token);
return {
...user,
token,
};
}
/**
* Finds the user by the access token
*/
async userFromToken(token) {
const user = await this.getUserInfo(token);
return {
...user,
token: { token, type: 'bearer' },
};
}
}
exports.AppleDriver = AppleDriver;