@devoven/oauth
v0.1.0
Published
OAuth 2.0 / OIDC module for NestJS — hexagonal architecture
Readme
@devoven/oauth
OAuth 2.0 / OIDC module for multi-provider authentication in NestJS. Wire up Google, LINE, or any custom provider with a single import.
Installation
npm install @devoven/oauth
# or
pnpm add @devoven/oauthPeer Dependencies
Install the standard NestJS validation stack if your app does not already have it:
npm install class-validator class-transformer@nestjs/common, @nestjs/core, rxjs, and reflect-metadata are expected to already be present in any NestJS application.
google-auth-library is bundled with this package and does not need to be installed separately.
Quick Start
import { OAuthModule, GoogleOAuthProvider } from '@devoven/oauth';
@Module({
imports: [
OAuthModule.register({
providers: [
{
name: 'google',
provider: GoogleOAuthProvider,
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackUrl: 'https://example.com/oauth/google/callback',
},
],
}),
],
})
export class AppModule {}This registers the OAuthController at /oauth, giving you:
GET /oauth/google— redirects the user to Google's consent screenGET /oauth/google/callback— handles the code exchange and returns the resolved profile
Module Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| providers | Array<OAuthProviderOption \| OAuthProviderPort> | — (required) | Provider configs or pre-constructed provider instances |
| stateStore | Class or instance of OAuthStateStorePort | InMemoryStateStore | CSRF state store. Default TTL is 5 minutes |
| controller | boolean | true | Mount OAuthController. Set to false to handle routes yourself |
| callbackSuccessUrl | string | undefined | When set, the callback endpoint redirects to this URL with profile fields as query parameters instead of returning JSON |
OAuthProviderOption
| Field | Type | Description |
|-------|------|-------------|
| name | string | Key used in the URL path (e.g. 'google' → /oauth/google) |
| provider | Constructor | A class implementing OAuthProviderPort (e.g. GoogleOAuthProvider) |
| clientId | string | OAuth app client ID |
| clientSecret | string | OAuth app client secret |
| callbackUrl | string | Absolute URL registered with the provider |
| scopes | string[] | Optional scope list. Defaults to the provider's built-in defaults |
Alternatively, pass a pre-constructed OAuthProviderPort instance directly in the providers array.
Async Registration
import { OAuthModule, GoogleOAuthProvider, LineOAuthProvider } from '@devoven/oauth';
import { ConfigService } from '@nestjs/config';
@Module({
imports: [
OAuthModule.registerAsync({
useFactory: (config: ConfigService) => ({
callbackSuccessUrl: config.get('OAUTH_SUCCESS_URL'),
providers: [
{
name: 'google',
provider: GoogleOAuthProvider,
clientId: config.get('GOOGLE_CLIENT_ID'),
clientSecret: config.get('GOOGLE_CLIENT_SECRET'),
callbackUrl: config.get('GOOGLE_CALLBACK_URL'),
},
{
name: 'line',
provider: LineOAuthProvider,
clientId: config.get('LINE_CLIENT_ID'),
clientSecret: config.get('LINE_CLIENT_SECRET'),
callbackUrl: config.get('LINE_CALLBACK_URL'),
},
],
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}REST API
All endpoints are served under the /oauth prefix.
Redirect to provider
GET /oauth/:providerGenerates a CSRF state token, stores it, and issues a 302 redirect to the provider's authorization URL.
| Parameter | Where | Description |
|-----------|-------|-------------|
| provider | path | Provider name as registered in providers (e.g. google, line) |
Response: 302 Found — Location header points to the provider's authorization URL.
Handle callback
GET /oauth/:provider/callback?code=...&state=...Verifies the state, exchanges the authorization code for tokens, fetches the user profile, and returns it.
| Parameter | Where | Description |
|-----------|-------|-------------|
| provider | path | Provider name |
| code | query | Authorization code from the provider |
| state | query | CSRF state value returned by the provider |
Response without callbackSuccessUrl: 200 OK
{
"provider": "google",
"providerId": "1234567890",
"email": "[email protected]",
"name": "Jane Doe",
"avatarUrl": "https://example.com/avatar.jpg"
}Fields email, name, and avatarUrl may be null depending on the scopes granted and the provider's response.
Response with callbackSuccessUrl: 302 Found — redirects to callbackSuccessUrl with the profile fields appended as query parameters (provider, providerId, email, name, avatarUrl).
Built-in Providers
GoogleOAuthProvider
Default scopes: openid profile email
Verifies the id_token using the Google auth library when present; falls back to the UserInfo endpoint otherwise.
LineOAuthProvider
Default scopes: openid profile
Verifies the id_token via LINE's /oauth2/v2.1/verify endpoint when present; falls back to the UserInfo endpoint otherwise.
Both classes accept a custom scopes array to override the defaults.
Architecture
Port / Token Mapping
| DI Token | Interface | Default Implementation | Purpose |
|----------|-----------|------------------------|---------|
| 'GetAuthorizationUrlPort' | GetAuthorizationUrlPort | GetAuthorizationUrlUseCase | Generate a provider authorization URL with a fresh state token |
| 'HandleCallbackPort' | HandleCallbackPort | HandleCallbackUseCase | Verify state, exchange code, return OAuthProfile |
| 'OAuthProviderRegistry' | OAuthProviderRegistry (Map<string, OAuthProviderPort>) | Built from providers option | Registry of provider name → provider instance |
| 'OAuthStateStorePort' | OAuthStateStorePort | InMemoryStateStore | Generate and verify CSRF state tokens |
GetAuthorizationUrlPort and HandleCallbackPort are exported from the module.
Domain Model
OAuthProfile
| Property | Type | Description |
|----------|------|-------------|
| provider | string | Provider name (e.g. 'google') |
| providerId | string | Provider-scoped user ID |
| email | string \| null | User email, if available |
| name | string \| null | Display name, if available |
| avatarUrl | string \| null | Profile picture URL, if available |
| id | string | Composite key: provider:providerId |
OAuthToken
| Property | Type | Description |
|----------|------|-------------|
| accessToken | string | Bearer token for API calls |
| refreshToken | string \| null | Refresh token, if issued |
| expiresAt | Date \| null | Computed from expiresIn when not provided directly |
| idToken | string \| null | OIDC ID token, if present |
| isExpired | boolean | true when expiresAt is in the past |
Custom Adapters
Custom state store
Provide a class or instance that implements OAuthStateStorePort for persistent CSRF state (e.g. Redis):
import { Injectable } from '@nestjs/common';
import { OAuthStateStorePort } from '@devoven/oauth';
@Injectable()
export class RedisStateStore implements OAuthStateStorePort {
async generate(): Promise<string> {
const state = crypto.randomUUID();
await redis.set(`oauth:state:${state}`, '1', 'EX', 300);
return state;
}
async verify(state: string): Promise<boolean> {
const key = `oauth:state:${state}`;
const exists = await redis.del(key);
return exists === 1;
}
}OAuthModule.register({
providers: [...],
stateStore: RedisStateStore,
})Custom provider
Implement OAuthProviderPort to add any OAuth 2.0 / OIDC provider:
import { OAuthProviderPort, OAuthProviderConfig, OAuthProfile, OAuthToken } from '@devoven/oauth';
export class GitHubOAuthProvider implements OAuthProviderPort {
readonly name = 'github';
constructor(private readonly config: OAuthProviderConfig) {}
async getAuthorizationUrl(state: string): Promise<string> { /* ... */ }
async exchangeCode(code: string): Promise<OAuthToken> { /* ... */ }
async getProfile(token: OAuthToken): Promise<OAuthProfile> { /* ... */ }
}Pass it either as a config object (the module will construct it) or as an already-instantiated object:
// Config object — module constructs the instance
OAuthModule.register({
providers: [
{
name: 'github',
provider: GitHubOAuthProvider,
clientId: '...',
clientSecret: '...',
callbackUrl: 'https://example.com/oauth/github/callback',
},
],
})
// Pre-constructed instance
OAuthModule.register({
providers: [new GitHubOAuthProvider({ clientId: '...', clientSecret: '...', callbackUrl: '...', scopes: [] })],
})