alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
106 lines (98 loc) • 3.43 kB
text/typescript
import { $context, AlephaError, z } from "alepha";
import type { IssuerPrimitive } from "alepha/security";
import {
$auth,
type LinkAccountFn,
type OidcOptions,
type WithLinkFn,
} from "./$auth.ts";
/**
* Creates an authentication provider primitive for France Connect.
*
* Uses OpenID Connect (OIDC) to authenticate users via France Connect,
* the French government's identity federation system. It provides verified
* identity data (name, email, birthdate) sourced directly from government
* databases.
*
* **France Connect-specific behaviour**:
* - Scopes use individual claim names (`given_name`, `family_name`) rather
* than the standard grouped `profile` scope.
* - The `acr_values=eidas1` authorization parameter is mandatory and is
* included automatically.
* - Logout is mandatory in France Connect integrations. Store the `id_token`
* returned at login and pass it to the logout endpoint when the session ends.
*
* **Environment Variables** (obtain from partenaires.franceconnect.gouv.fr):
* - `FRANCECONNECT_CLIENT_ID` — OAuth 2.0 client ID for your France Connect service provider.
* - `FRANCECONNECT_CLIENT_SECRET` — OAuth 2.0 client secret for your France Connect service provider.
*
* @example
* ```ts
* class AuthProviders {
* franceconnect = $authFranceConnect(this.userRealm);
* }
* ```
*/
export const $authFranceConnect = (
realm: IssuerPrimitive & WithLinkFn,
options: Partial<OidcOptions> = {},
) => {
const { alepha } = $context();
const env = alepha.parseEnv(
z.object({
FRANCECONNECT_CLIENT_ID: z
.text({
description:
"The OAuth 2.0 client ID for your France Connect service provider, obtained from partenaires.franceconnect.gouv.fr.",
})
.optional(),
FRANCECONNECT_CLIENT_SECRET: z
.text({
description:
"The OAuth 2.0 client secret for your France Connect service provider, obtained from partenaires.franceconnect.gouv.fr.",
})
.optional(),
}),
);
const disabled =
!env.FRANCECONNECT_CLIENT_ID || !env.FRANCECONNECT_CLIENT_SECRET;
const name = "franceconnect";
const account: LinkAccountFn | undefined =
options.account ?? (realm.link ? realm.link(name) : undefined);
if (!account) {
throw new AlephaError(
"Authentication requires a link function in the realm primitive.",
);
}
return $auth({
issuer: realm,
name,
oidc: {
/**
* France Connect production OIDC issuer.
* Discovery: https://oidc.franceconnect.gouv.fr/api/v2/.well-known/openid-configuration
*
* Note: `oidc.franceconnect.gouv.fr` is standard FranceConnect (eidas1).
* `auth.franceconnect.gouv.fr` is FranceConnect+ (eidas2/eidas3).
*/
issuer: "https://oidc.franceconnect.gouv.fr/api/v2",
clientId: env.FRANCECONNECT_CLIENT_ID!,
clientSecret: env.FRANCECONNECT_CLIENT_SECRET,
/**
* France Connect requires individual claim names as scopes.
* The standard grouped `profile` scope is NOT supported.
*/
scope: "openid given_name family_name email",
/**
* `acr_values=eidas1` is mandatory for all France Connect integrations.
*/
...options,
authorizationParameters: {
acr_values: "eidas1",
...options.authorizationParameters,
},
account,
},
disabled,
});
};