Skip to content

Commit

Permalink
feat: add api client integration
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Oct 27, 2023
1 parent d43d89b commit 2fb7b72
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 3 deletions.
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
".": "./build/index.js",
"./types": "./build/src/auth/types.js",
"./auth_provider": "./build/providers/auth_provider.js",
"./plugins/api_client": "./build/src/auth/plugins/japa/api_client.js",
"./services/main": "./build/services/auth.js",
"./core/token": "./build/src/core/token.js",
"./core/guard_user": "./build/src/core/guard_user.js",
Expand Down Expand Up @@ -74,6 +75,7 @@
"@adonisjs/tsconfig": "^1.1.8",
"@commitlint/cli": "^18.0.0",
"@commitlint/config-conventional": "^18.0.0",
"@japa/api-client": "^2.0.0",
"@japa/assert": "^2.0.0",
"@japa/expect-type": "^2.0.0",
"@japa/file-system": "^2.0.0",
Expand Down Expand Up @@ -132,16 +134,20 @@
"@poppinss/utils": "^6.5.0"
},
"peerDependencies": {
"@adonisjs/core": "^6.1.5-30",
"@adonisjs/core": "^6.1.5-31",
"@adonisjs/lucid": "^19.0.0-3",
"@adonisjs/session": "^7.0.0-13"
"@adonisjs/session": "^7.0.0-13",
"@japa/api-client": "^2.0.0"
},
"peerDependenciesMeta": {
"@adonisjs/lucid": {
"optional": true
},
"@adonisjs/session": {
"optional": true
},
"@japa/api-client": {
"optional": true
}
}
}
8 changes: 8 additions & 0 deletions src/auth/auth_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { HttpContext } from '@adonisjs/core/http'

import type { GuardFactory } from './types.js'
import { Authenticator } from './authenticator.js'
import { AuthenticatorClient } from './authenticator_client.js'

/**
* Auth manager exposes the API to register and manage authentication
Expand Down Expand Up @@ -42,4 +43,11 @@ export class AuthManager<KnownGuards extends Record<string, GuardFactory>> {
createAuthenticator(ctx: HttpContext) {
return new Authenticator<KnownGuards>(ctx, this.#config)
}

/**
* Creates an instance of the authenticator client
*/
createAuthenticatorClient() {
return new AuthenticatorClient<KnownGuards>(this.#config)
}
}
73 changes: 73 additions & 0 deletions src/auth/authenticator_client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* @adonisjs/auth
*
* (c) AdonisJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import debug from './debug.js'
import type { GuardFactory } from './types.js'
import { HttpContextFactory } from '@adonisjs/core/factories/http'

/**
* Authenticator client is used to create guard instances for
* testing. It passes a fake HTTPContext to the guards, so
* make sure to not call server side APIs that might be
* relying on a real HTTPContext instance
*/
export class AuthenticatorClient<KnownGuards extends Record<string, GuardFactory>> {
/**
* Registered guards
*/
#config: {
default: keyof KnownGuards
guards: KnownGuards
}

/**
* Cache of guards
*/
#guardsCache: Partial<Record<keyof KnownGuards, unknown>> = {}

/**
* Name of the default guard
*/
get defaultGuard(): keyof KnownGuards {
return this.#config.default
}

constructor(config: { default: keyof KnownGuards; guards: KnownGuards }) {
this.#config = config
debug('creating authenticator client. config %O', this.#config)
}

/**
* Returns an instance of a known guard. Guards instances are
* cached during the lifecycle of an HTTP request.
*/
use<Guard extends keyof KnownGuards>(guard?: Guard): ReturnType<KnownGuards[Guard]> {
const guardToUse = guard || this.#config.default

/**
* Use cached copy if exists
*/
const cachedGuard = this.#guardsCache[guardToUse]
if (cachedGuard) {
debug('using guard from cache. name: "%s"', guardToUse)
return cachedGuard as ReturnType<KnownGuards[Guard]>
}

const guardFactory = this.#config.guards[guardToUse]

/**
* Construct guard and cache it
*/
debug('creating guard. name: "%s"', guardToUse)
const guardInstance = guardFactory(new HttpContextFactory().create())
this.#guardsCache[guardToUse] = guardInstance

return guardInstance as ReturnType<KnownGuards[Guard]>
}
}
108 changes: 108 additions & 0 deletions src/auth/plugins/japa/api_client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* @adonisjs/auth
*
* (c) AdonisJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/// <reference types="@adonisjs/session/plugins/api_client" />

import { ApiClient, ApiRequest } from '@japa/api-client'
import type { ApplicationService } from '@adonisjs/core/types'
import type { Authenticators, GuardContract, GuardFactory } from '../../types.js'

declare module '@japa/api-client' {
export interface ApiRequest {
authData: {
guard: string
user: unknown
}

/**
* Login a user using the default authentication
* guard when making an API call
*/
loginAs(user: {
[K in keyof Authenticators]: Authenticators[K] extends GuardFactory
? ReturnType<Authenticators[K]> extends GuardContract<infer A>
? A
: never
: never
}): this

/**
* Define the authentication guard for login
*/
withGuard<K extends keyof Authenticators, Self extends ApiRequest>(
this: Self,
guard: K
): {
/**
* Login a user using a specific auth guard
*/
loginAs(
user: Authenticators[K] extends GuardFactory
? ReturnType<Authenticators[K]> extends GuardContract<infer A>
? A
: never
: never
): Self
}
}
}

/**
* Auth API client to authenticate users when making
* HTTP requests using the Japa API client
*/
export const authApiClient = (app: ApplicationService) => {
ApiRequest.macro('loginAs', function (this: ApiRequest, user) {
this.authData = {
guard: '__default__',
user: user,
}
return this
})

ApiRequest.macro('withGuard', function <
K extends keyof Authenticators,
Self extends ApiRequest,
>(this: Self, guard: K) {
return {
loginAs: (user) => {
this.authData = {
guard,
user: user,
}
return this
},
}
})

/**
* Hook into the request and login the user
*/
ApiClient.setup(async (request) => {
const auth = await app.container.make('auth.manager')
const authData = request['authData']
if (!authData) {
return
}

const client = auth.createAuthenticatorClient()
const guard = authData.guard === '__default__' ? client.use() : client.use(authData.guard)
const requestData = await (guard as GuardContract<unknown>).authenticateAsClient(authData.user)

if (requestData.headers) {
request.headers(requestData.headers)
}
if (requestData.session) {
request.withSession(requestData.session)
}
if (requestData.cookies) {
request.cookies(requestData.cookies)
}
})
}
16 changes: 16 additions & 0 deletions src/auth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ import type { ApplicationService, ConfigProvider } from '@adonisjs/core/types'
import type { AuthManager } from './auth_manager.js'
import type { GUARD_KNOWN_EVENTS } from './symbols.js'

/**
* The client response for authentication.
*/
export interface AuthClientResponse {
headers?: Record<string, any>
cookies?: Record<string, any>
session?: Record<string, any>
}

/**
* A set of properties a guard must implement.
*/
Expand Down Expand Up @@ -46,6 +55,13 @@ export interface GuardContract<User> {
*/
check(): Promise<boolean>

/**
* The method is used to authenticate the user as
* client. This method should return cookies,
* headers, or session state.
*/
authenticateAsClient(user: User): Promise<AuthClientResponse>

/**
* Authenticates the current request and throws
* an exception if the request is not authenticated.
Expand Down
20 changes: 19 additions & 1 deletion src/guards/session/guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Exception, RuntimeException } from '@poppinss/utils'

import debug from '../../auth/debug.js'
import { RememberMeToken } from './token.js'
import type { GuardContract } from '../../auth/types.js'
import type { AuthClientResponse, GuardContract } from '../../auth/types.js'

Check failure on line 18 in src/guards/session/guard.ts

View workflow job for this annotation

GitHub Actions / typecheck / typecheck

'AuthClientResponse' is declared but never used.
import { GUARD_KNOWN_EVENTS, PROVIDER_REAL_USER } from '../../auth/symbols.js'
import { AuthenticationException, InvalidCredentialsException } from '../../auth/errors.js'
import type {
Expand Down Expand Up @@ -629,4 +629,22 @@ export class SessionGuard<UserProvider extends SessionUserProviderContract<unkno
debug('session_auth: deleting remember me token')
await this.#rememberMeTokenProvider.deleteTokenBySeries(decodedToken.series)
}

/**
* Returns the session state for the user to be
* logged-in as a client
*/
async authenticateAsClient(
user: UserProvider[typeof PROVIDER_REAL_USER]
): Promise<{ session: Record<string, string | number> }> {
const providerUser = await this.#userProvider.createUserForGuard(user)
const userId = providerUser.getId()

debug('session_guard: returning client session for user id "%s"', userId)
return {
session: {
[this.sessionKeyName]: userId,
},
}
}
}

0 comments on commit 2fb7b72

Please sign in to comment.