@oa2/core
Version:
A comprehensive, RFC-compliant OAuth 2.0 authorization server implementation in TypeScript
338 lines (333 loc) • 13.6 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
var errors_cjs = require('./errors.cjs');
var utils_cjs = require('./utils.cjs');
/**
* Creates a token object with the given parameters.
* Helper function to ensure consistency across token creation.
*/ function createToken(params, accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt) {
return {
accessToken,
accessTokenExpiresAt,
refreshToken,
refreshTokenExpiresAt,
scope: params.scope,
clientId: params.client.id,
userId: params.userId
};
}
/**
* Validates a token by checking if it exists and is not expired.
* Returns null if the token is invalid or expired.
*/ async function validateTokenInStorage(tokenValue, storage, getTokenFn, expiresAtProperty) {
const token = await getTokenFn(tokenValue);
if (!token) {
return null;
}
// Check if token is expired
const expiresAt = token[expiresAtProperty];
if (expiresAt && expiresAt <= new Date()) {
// Optionally revoke expired token
await storage.revokeToken(tokenValue);
return null;
}
return token;
}
/**
* Generates an access token using the opaque strategy.
* Creates a random token and stores it in the database.
*/ async function generateAccessToken(params, context, options, storage) {
const accessToken = utils_cjs.generateRandomString(options.tokenLength || 32);
const accessTokenExpiresAt = new Date(Date.now() + (options.accessTokenExpiresIn || 3600) * 1000);
const token = createToken(params, accessToken, accessTokenExpiresAt);
await storage.saveToken(token);
return token;
}
/**
* Generates a refresh token using the opaque strategy.
* Creates a random token and stores it in the database.
*/ async function generateRefreshToken(params, context, options, storage) {
const refreshToken = utils_cjs.generateRandomString(options.tokenLength || 32);
const refreshTokenExpiresAt = new Date(Date.now() + (options.refreshTokenExpiresIn || 604800) * 1000);
const token = createToken(params, '', new Date(), refreshToken, refreshTokenExpiresAt);
await storage.saveToken(token);
return token;
}
/**
* Generates both access and refresh tokens in a single operation.
* More efficient when both tokens are needed.
*/ async function generateTokenPair(params, context, options, storage) {
const accessToken = utils_cjs.generateRandomString(options.tokenLength || 32);
const refreshToken = utils_cjs.generateRandomString(options.tokenLength || 32);
const accessTokenExpiresAt = new Date(Date.now() + (options.accessTokenExpiresIn || 3600) * 1000);
const refreshTokenExpiresAt = new Date(Date.now() + (options.refreshTokenExpiresIn || 604800) * 1000);
const token = createToken(params, accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt);
await storage.saveToken(token);
return token;
}
/**
* Validates an access token by looking it up in the database.
*/ async function validateAccessToken(accessToken, context) {
return validateTokenInStorage(accessToken, context.storage, (token)=>context.storage.getAccessToken(token), 'accessTokenExpiresAt');
}
/**
* Validates a refresh token by looking it up in the database.
*/ async function validateRefreshToken(refreshToken, context) {
return validateTokenInStorage(refreshToken, context.storage, (token)=>context.storage.getRefreshToken(token), 'refreshTokenExpiresAt');
}
/**
* Creates an Opaque Token Strategy.
*
* Opaque tokens are random strings stored in the database. They provide maximum
* security as tokens can be easily revoked and contain no embedded information.
* Validation requires database lookups but provides fine-grained control.
*
* @param storage The storage adapter for token persistence
* @param options Opaque token configuration options
*
* @example
* ```typescript
* const storage = new YourStorageAdapter();
* const tokenStrategy = createOpaqueTokenStrategy(storage, {
* accessTokenExpiresIn: 3600, // 1 hour
* refreshTokenExpiresIn: 604800, // 7 days
* tokenLength: 32
* });
* ```
*/ function createOpaqueTokenStrategy(storage, options = {}) {
return {
generateAccessToken: (params, context)=>generateAccessToken(params, context, options, storage),
generateRefreshToken: (params, context)=>generateRefreshToken(params, context, options, storage),
generateTokenPair: (params, context)=>generateTokenPair(params, context, options, storage),
validateAccessToken: (accessToken, context)=>validateAccessToken(accessToken, context),
validateRefreshToken: (refreshToken, context)=>validateRefreshToken(refreshToken, context)
};
}
/**
* Authenticates a client using the provided credentials.
* Supports both Basic authentication and form-based credentials.
*/ async function authenticateClient(request, storage) {
const body = utils_cjs.parseRequestBody(request);
const { client_id, client_secret } = body;
let authenticatedClientId;
let authenticatedClientSecret;
// Try Basic authentication first
const basicAuth = utils_cjs.extractBasicAuthCredentials(request.headers.authorization || request.headers.Authorization);
if (basicAuth) {
authenticatedClientId = basicAuth.clientId;
authenticatedClientSecret = basicAuth.clientSecret;
} else if (client_id && client_secret) {
// Fall back to form-based authentication
authenticatedClientId = client_id;
authenticatedClientSecret = client_secret;
} else if (client_id) {
// Public client (no secret required)
authenticatedClientId = client_id;
} else {
throw new errors_cjs.InvalidRequestError('Client authentication required');
}
if (!authenticatedClientId || authenticatedClientId.trim() === '') {
throw new errors_cjs.UnauthorizedClientError('Client not found');
}
const client = await storage.getClient(authenticatedClientId);
if (!client) {
throw new errors_cjs.UnauthorizedClientError('Client not found');
}
// Verify client secret if provided
if (authenticatedClientSecret && client.secret) {
if (!utils_cjs.verifyClientSecret(authenticatedClientSecret, client.secret)) {
throw new errors_cjs.UnauthorizedClientError('Invalid client credentials');
}
}
return client;
}
/**
* Finds a grant that supports the specified response type.
* Used for authorization endpoint requests.
*/ function findGrantByResponseType(grants, responseType) {
const responseTypeGrants = grants.filter((grant)=>grant.handleAuthorization && grant.responseTypes?.includes(responseType));
const grant = responseTypeGrants[0];
if (!grant) {
throw new errors_cjs.UnsupportedResponseTypeError(`Unsupported response_type: ${responseType}`);
}
return grant;
}
/**
* Finds a grant that supports the specified grant type.
* Used for token endpoint requests.
*/ function findGrantByType(grants, grantType) {
const grant = grants.find((g)=>g.type === grantType);
if (!grant) {
throw new errors_cjs.UnsupportedGrantTypeError(`Unsupported grant_type: ${grantType}`);
}
return grant;
}
/**
* Creates a complete server configuration with defaults.
* Ensures all required fields are present and valid.
*/ function createCompleteConfig(config) {
return {
...config,
tokenStrategy: config.tokenStrategy || createOpaqueTokenStrategy(config.storage, {
accessTokenExpiresIn: config.accessTokenLifetime || 3600,
refreshTokenExpiresIn: config.refreshTokenLifetime || 604800
})
};
}
/**
* Creates a context object for grant handlers.
* Includes all necessary information for processing OAuth 2.0 requests.
*/ function createContext(request, storage, client, config) {
return {
request,
storage,
client,
config
};
}
/**
* Processes an authorization request and delegates to the appropriate grant handler.
* Validates the request parameters and client before proceeding.
*/ async function handleAuthorizeRequest(request, storage, config) {
const { client_id, response_type } = request.query;
if (!client_id) {
throw new errors_cjs.InvalidRequestError('Missing client_id parameter');
}
if (!response_type) {
throw new errors_cjs.InvalidRequestError('Missing response_type parameter');
}
// Find the appropriate grant for this response type
const grant = findGrantByResponseType(config.grants, response_type);
// Get the client
const client = await storage.getClient(client_id);
if (!client) {
throw new errors_cjs.UnauthorizedClientError('Client not found');
}
// Create context and delegate to grant handler
const context = createContext(request, storage, client, config);
if (!grant.handleAuthorization) {
throw new errors_cjs.UnsupportedResponseTypeError(`Grant does not support authorization requests: ${response_type}`);
}
return grant.handleAuthorization(context);
}
/**
* Token Endpoint
* ==============
* Handles OAuth 2.0 token requests.
*/ /**
* Processes a token request and delegates to the appropriate grant handler.
* Performs client authentication and grant type validation.
*/ async function handleTokenRequest(request, storage, config) {
const body = utils_cjs.parseRequestBody(request);
const { grant_type } = body;
if (!grant_type) {
throw new errors_cjs.InvalidRequestError('Missing grant_type parameter');
}
// Authenticate the client
const client = await authenticateClient(request, storage);
// Find the appropriate grant for this grant type
const grant = findGrantByType(config.grants, grant_type);
// Create context and delegate to grant handler
const context = createContext(request, storage, client, config);
if (!grant.handleToken) {
throw new errors_cjs.UnsupportedGrantTypeError(`Grant does not support token requests: ${grant_type}`);
}
return grant.handleToken(context);
}
/**
* Revocation Endpoint
* ===================
* Handles OAuth 2.0 token revocation requests.
*/ /**
* Processes a token revocation request.
* Validates the token parameter and revokes the specified token.
*/ async function handleRevokeRequest(request, storage) {
const body = utils_cjs.parseRequestBody(request);
const { token } = body;
if (!token) {
throw new errors_cjs.InvalidRequestError('Missing token parameter');
}
// In a real implementation, you would validate the client making the revocation request
// and ensure they are authorized to revoke this token.
await storage.revokeToken(token);
return {
statusCode: 200,
headers: {},
body: {},
cookies: {}
};
}
/**
* Processes a token introspection request.
* Returns metadata about the specified token.
*/ async function handleIntrospectRequest(request, storage) {
const body = utils_cjs.parseRequestBody(request);
const { token } = body;
if (!token) {
throw new errors_cjs.InvalidRequestError('Missing token parameter');
}
const accessToken = await storage.getAccessToken(token);
const refreshToken = await storage.getRefreshToken(token);
let active = false;
let responseBody = {
active: false
};
if (accessToken) {
active = accessToken.accessTokenExpiresAt > new Date();
if (active) {
responseBody = {
active: true,
scope: accessToken.scope,
client_id: accessToken.clientId,
username: accessToken.userId,
exp: Math.floor(accessToken.accessTokenExpiresAt.getTime() / 1000)
};
}
} else if (refreshToken) {
active = refreshToken.refreshTokenExpiresAt ? refreshToken.refreshTokenExpiresAt > new Date() : false;
if (active) {
responseBody = {
active: true,
scope: refreshToken.scope,
client_id: refreshToken.clientId,
username: refreshToken.userId,
exp: Math.floor(refreshToken.refreshTokenExpiresAt.getTime() / 1000)
};
}
}
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json'
},
body: responseBody,
cookies: {}
};
}
/**
* Creates and configures an OAuth 2.0 server instance.
* Provides a clean, functional interface for handling OAuth 2.0 flows.
*
* @example
* ```typescript
* const storage = new MyStorageAdapter();
* const server = createOAuth2Server({
* storage,
* grants: [createAuthorizationCodeGrant(), clientCredentialsGrant()],
* predefinedScopes: ['read', 'write'],
* tokenStrategy: createJwtTokenStrategy(storage, { secret: 'my-secret' })
* });
* ```
*/ function createOAuth2Server(config) {
const completeConfig = createCompleteConfig(config);
const { storage } = completeConfig;
return {
authorize: (request)=>handleAuthorizeRequest(request, storage, completeConfig),
token: (request)=>handleTokenRequest(request, storage, completeConfig),
revoke: (request)=>handleRevokeRequest(request, storage),
introspect: (request)=>handleIntrospectRequest(request, storage)
};
}
// For backward compatibility
const createServer = createOAuth2Server;
exports.createOAuth2Server = createOAuth2Server;
exports.createServer = createServer;