import type { RefedEntrypointHandler } from '@owlmeans/server-entrypoint' import { handleBody } from '@owlmeans/server-api' import type { OIDCAuthInitParams, OidcProviderDescriptor } from '@owlmeans/oidc' import { AuthenPayloadError, AuthUnknown, DISPATCHER } from '@owlmeans/auth' import { assertContext } from '@owlmeans/context' import type { Config, Context, OidcClientAdapter, OidcClientService } from '../types.js' import type { ClientEntrypoint } from '@owlmeans/client-entrypoint' import type { CommonEntrypoint } from '@owlmeans/entrypoint' import { makeSecurityHelper } from '@owlmeans/config' import { authService, DEFAULT_ALIAS } from '../consts.js' // import { PROVIDER_CACHE_TTL } from '../consts.js' // import type { Client } from 'openid-client' import { base64urlnopad as base64 } from '@scure/base' import { randomBytes } from '@noble/hashes/utils.js' import { sha256 } from '@noble/hashes/sha2.js' import { oidcCacheOf } from '../utils/cache.js' import { requestedScope } from '../utils/scope.js' import { AUTHEN_TIMEFRAME } from '@owlmeans/server-auth' export const init: RefedEntrypointHandler = handleBody(async (body: OIDCAuthInitParams, ctx) => { if (body.entity == null) { throw new AuthenPayloadError('entity') } const entityId = body.entity const context = assertContext(ctx) const oidc = context.service(DEFAULT_ALIAS) let client: OidcClientAdapter | null = null // Only the remote-lookup branch below actually resolves the client BY entityId. The default // provider is picked by `def: true` alone, so persisting the caller-supplied entityId // unconditionally records a value that was never verified against the registered provider — // and the exchange step (utils/auth.ts) later requires an exact match on it, failing even // though the very same default provider is trivially available there too. let entityIdUsedForResolution = false const defaultClientId = oidc.getDefault() if (defaultClientId != null) { client = await oidc.getClient(defaultClientId) } if (client == null) { entityIdUsedForResolution = true // if (!oidc.hasProvider({ entityId })) { /** * @TODO We need to move it to some remote resource. * And make oidc service itself use such a resource to get required client. */ const providers = await context.entrypoint>( authService.provider.list ).call({ params: { service: context.cfg.alias ?? context.cfg.service }, query: { entityId } }) if (providers.length < 1) { throw new AuthUnknown() } const provider = providers.find(p => p.def === true) ?? providers.shift() // Loaded providers can't be default otherwise we will get stuck with one of them if (provider != null && "def" in provider) { delete provider.def } if (provider == null) { throw new AuthUnknown() } oidc.registerTemporaryProvider(provider) client = await oidc.getClient({ clientId: provider.clientId, entityId }) } // Cache provider for a while - we dont actually need to clean it up // setTimeout(() => provider && oidc.unregisterTemporaryProvider(provider), PROVIDER_CACHE_TTL) // } else { // client = await oidc.getClient({ // entityId, clientId: oidc.entityToClientId({ entityId }) // }) // } const verifier = base64.encode(randomBytes(32)) const challenge = base64.encode(sha256(new TextEncoder().encode(verifier))) const oidcCache = oidcCacheOf(context) await oidcCache.resource().create({ id: oidcCache.verifierId(challenge), verifier, client: client.getClientId(), ...(entityIdUsedForResolution ? { entityId } : {}), // Only a request: the exchange honours it when the provider names that organization among the // subject's, and starts the session in the home one otherwise. ...(body.entitySlug != null ? { entitySlug: body.entitySlug } : {}), }, { ttl: AUTHEN_TIMEFRAME / 1000 }) // The dispatcher is a FRONTEND route, and this is a server context — the entrypoint registered // here has no `url()` (that helper is attached by `@owlmeans/client-entrypoint` only, for a // browser context that can resolve its own address). Assembled the way `server-oidc-provider`'s // interaction URL is: the entrypoint's path, qualified with the address it answers on — the // frontend service's, not this backend's. const dispatcherEntry = context.entrypoint(DISPATCHER) const dispatcherUrl = makeSecurityHelper(context) .makeUrl(dispatcherEntry.address(), dispatcherEntry.path()) const cfg = client.getConfig() const url = client.makeAuthUrl({ scope: requestedScope(cfg?.extraScopes), code_challenge: challenge, code_challenge_method: 'S256', redirect_uri: dispatcherUrl, }) return url })