@wristband/express-auth
Version:
SDK for integrating your ExpressJS application with Wristband. Handles user authentication and token management.
122 lines (121 loc) • 5.44 kB
JavaScript
import { WristbandApiClient } from './wristband-api-client';
import { JSON_MEDIA_TYPE } from './utils/constants';
import { InvalidGrantError } from './error';
export class WristbandService {
constructor(wristbandApplicationDomain, clientId, clientSecret) {
if (!wristbandApplicationDomain || !wristbandApplicationDomain.trim()) {
throw new Error('Wristband application domain is required');
}
if (!clientId || !clientId.trim()) {
throw new Error('Client ID is required');
}
if (!clientSecret || !clientSecret.trim()) {
throw new Error('Client secret is required');
}
this.wristbandApiClient = new WristbandApiClient(wristbandApplicationDomain);
this.clientId = clientId;
this.clientSecret = clientSecret;
this.basicAuthConfig = {
auth: {
username: this.clientId,
password: this.clientSecret,
},
};
}
async getTokens(code, redirectUri, codeVerifier) {
if (!code || !code.trim()) {
throw new Error('Authorization code is required');
}
if (!redirectUri || !redirectUri.trim()) {
throw new Error('Redirect URI is required');
}
if (!codeVerifier || !codeVerifier.trim()) {
throw new Error('Code verifier is required');
}
try {
const formParams = `grant_type=authorization_code&code=${code}&redirect_uri=${redirectUri}&code_verifier=${codeVerifier}`;
const tokenResponse = await this.wristbandApiClient.axiosInstance.post('/oauth2/token', formParams, this.basicAuthConfig);
// Validate response data is a valid TokenResponse
WristbandService.validateTokenResponse(tokenResponse.data);
return tokenResponse.data;
}
catch (error) {
if (WristbandService.isAxiosError(error) && WristbandService.hasInvalidGrantError(error)) {
throw new InvalidGrantError(WristbandService.getErrorDescription(error) || 'Invalid grant');
}
throw error;
}
}
async getUserinfo(accessToken) {
if (!accessToken || !accessToken.trim()) {
throw new Error('Access token is required');
}
const bearerTokenConfig = {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': JSON_MEDIA_TYPE,
Accept: JSON_MEDIA_TYPE,
},
};
const response = await this.wristbandApiClient.axiosInstance.get('/oauth2/userinfo', bearerTokenConfig);
// Validate response data is a valid Userinfo object
if (typeof response.data !== 'object' || response.data === null) {
throw new Error('Invalid userinfo response');
}
return response.data;
}
async refreshToken(refreshToken) {
if (!refreshToken || !refreshToken.trim()) {
throw new Error('Refresh token is required');
}
try {
const formParams = `grant_type=refresh_token&refresh_token=${refreshToken}`;
const tokenResponse = await this.wristbandApiClient.axiosInstance.post('/oauth2/token', formParams, this.basicAuthConfig);
// Validate response data is a valid TokenResponse
WristbandService.validateTokenResponse(tokenResponse.data);
return tokenResponse.data;
}
catch (error) {
if (WristbandService.isAxiosError(error) && WristbandService.hasInvalidGrantError(error)) {
throw new InvalidGrantError(WristbandService.getErrorDescription(error) || 'Invalid refresh token');
}
throw error;
}
}
async revokeRefreshToken(refreshToken) {
if (!refreshToken || !refreshToken.trim()) {
throw new Error('Refresh token is required');
}
await this.wristbandApiClient.axiosInstance.post('/oauth2/revoke', `token=${refreshToken}`, this.basicAuthConfig);
}
// Helper method to check if an error is an Axios error
static isAxiosError(error) {
return !!error && typeof error === 'object' && 'isAxiosError' in error;
}
// Helper method to check if an error has an invalid_grant error
static hasInvalidGrantError(error) {
return (!!error.response?.data &&
typeof error.response.data === 'object' &&
'error' in error.response.data &&
error.response.data.error === 'invalid_grant');
}
// Helper method to get error description from an axios error
static getErrorDescription(error) {
if (error.response?.data && typeof error.response.data === 'object' && 'error_description' in error.response.data) {
return error.response.data.error_description;
}
return undefined;
}
// Helper method to validate token response
static validateTokenResponse(data) {
if (!data || typeof data !== 'object') {
throw new Error('Invalid token response');
}
if (!('access_token' in data) || typeof data.access_token !== 'string') {
throw new Error('Invalid token response: missing access_token');
}
if (!('expires_in' in data) || typeof data.expires_in !== 'number') {
throw new Error('Invalid token response: missing expires_in');
}
}
}