@basaltkit/auth-saml 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Basalt Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # @basaltkit/auth-saml
2
+
3
+ SAML 2.0 single sign-on for [`@basaltkit/auth`](https://www.npmjs.com/package/@basaltkit/auth). SP-initiated
4
+ login for enterprise IdPs (ADFS, Okta, OneLogin, Azure AD, Shibboleth…) that speak
5
+ SAML rather than OIDC.
6
+
7
+ Signature verification, XML canonicalization and the SAML protocol are handled by
8
+ [`@node-saml/node-saml`](https://github.com/node-saml/node-saml) — a vetted
9
+ XML-DSig implementation. This package only wires a validated assertion into
10
+ `Auth.socialLogin`, so a user proven by the IdP is logged in with the same tokens
11
+ as any other login. **This deliberately does not hand-roll SAML crypto.**
12
+
13
+ > For modern IdPs prefer OIDC — `@basaltkit/auth`'s `oidcProvider` /
14
+ > `discoverOidcProvider` cover Okta, Azure AD, Auth0, Google Workspace and
15
+ > Keycloak with no extra dependency. Reach for SAML only when the IdP requires it.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pnpm add @basaltkit/auth-saml @node-saml/node-saml # node-saml is a peer dependency
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```ts
26
+ import { authPlugin, authRoutes } from '@basaltkit/auth'
27
+ import { samlPlugin, samlRoutes } from '@basaltkit/auth-saml'
28
+
29
+ createApp({
30
+ plugins: [
31
+ authPlugin({ users, secret: env.APP_SECRET }),
32
+ fastifyPlugin({ routes: [...authRoutes(), ...samlRoutes()] }),
33
+ samlPlugin({
34
+ providers: [
35
+ {
36
+ name: 'okta',
37
+ entryPoint: 'https://acme.okta.com/app/…/sso/saml',
38
+ idpCert: env.OKTA_IDP_CERT, // the IdP's signing certificate (PEM)
39
+ issuer: 'https://app.example.com/sp', // your SP entity id
40
+ callbackUrl: 'https://app.example.com/auth/saml/okta/acs',
41
+ },
42
+ ],
43
+ }),
44
+ ],
45
+ })
46
+ ```
47
+
48
+ Three routes per provider:
49
+
50
+ | Route | Purpose |
51
+ |---|---|
52
+ | `GET /auth/saml/:provider/login` | Redirects the browser to the IdP (SP-initiated). |
53
+ | `POST /auth/saml/:provider/acs` | The IdP POSTs the signed `SAMLResponse` here; on a valid assertion the user is logged in. Responds with JSON tokens, or pass `samlRoutes({ successRedirect })` to bounce back to your SPA. |
54
+ | `GET /auth/saml/:provider/metadata` | SP metadata XML — hand it to the IdP admin to register the app. |
55
+
56
+ The user is matched by **email** (find-or-create, passwordless); a validated
57
+ assertion is trusted, so `emailVerified` is set. Read the email from a specific
58
+ attribute with `emailAttribute` on the provider (default: `email`, common email
59
+ claims, or an email-shaped `NameID`).
60
+
61
+ `samlPlugin` is adapter-agnostic — the Fastify, Express and Hono adapters all
62
+ parse the `application/x-www-form-urlencoded` ACS POST. Register it after `authPlugin`.
@@ -0,0 +1,110 @@
1
+ import * as _basaltkit_core from '@basaltkit/core';
2
+ import { BasaltError } from '@basaltkit/core';
3
+ import { Auth, PublicUser, TokenPair } from '@basaltkit/auth';
4
+ import { BasaltRoute } from '@basaltkit/http';
5
+
6
+ declare class SamlProviderUnknownError extends BasaltError {
7
+ readonly status = 404;
8
+ constructor(name: string);
9
+ }
10
+ declare class SamlResponseInvalidError extends BasaltError {
11
+ readonly status = 400;
12
+ constructor(detail?: string);
13
+ }
14
+ /** The subset of a node-saml profile we read. */
15
+ interface SamlProfile {
16
+ nameID?: string;
17
+ email?: string;
18
+ [claim: string]: unknown;
19
+ }
20
+ /**
21
+ * The node-saml surface this package uses — kept minimal so the underlying
22
+ * library (which does the XML-DSig verification) is injectable in tests.
23
+ */
24
+ interface SamlClient {
25
+ getAuthorizeUrlAsync(relayState: string, host: string | undefined, options: Record<string, unknown>): Promise<string>;
26
+ validatePostResponseAsync(container: Record<string, string>): Promise<{
27
+ profile: SamlProfile | null;
28
+ loggedOut: boolean;
29
+ }>;
30
+ generateServiceProviderMetadata(decryptionCert: string | null, signingCert?: string | null): string;
31
+ }
32
+ interface SamlProvider {
33
+ name: string;
34
+ /** IdP Single-Sign-On URL (HTTP-Redirect binding). */
35
+ entryPoint: string;
36
+ /** IdP signing certificate(s) (PEM). Used to verify the assertion signature. */
37
+ idpCert: string | string[];
38
+ /** SP entity id (this app's issuer). */
39
+ issuer: string;
40
+ /** ACS URL the IdP POSTs the SAMLResponse to. */
41
+ callbackUrl: string;
42
+ /** Attribute to read the email from. Default: `email` / common email claims / an email-shaped NameID. */
43
+ emailAttribute?: string;
44
+ }
45
+ interface SamlOptions {
46
+ /**
47
+ * Factory for the underlying SAML client. Default: `@node-saml/node-saml`.
48
+ * Injectable for tests so the crypto path is exercised by the real library
49
+ * in production but stubbed in unit tests.
50
+ */
51
+ createClient?: (provider: SamlProvider) => SamlClient;
52
+ /** Host used when building the AuthnRequest (optional). */
53
+ host?: string;
54
+ }
55
+ /** Extracts the user's email from a validated assertion. */
56
+ declare function extractEmail(profile: SamlProfile, attribute?: string): string | undefined;
57
+ /**
58
+ * SAML 2.0 SP-initiated SSO. Signature verification, canonicalization and the
59
+ * SAML protocol are delegated to `@node-saml/node-saml`; this only wires the
60
+ * result into {@link Auth.socialLogin}. A validated assertion is trusted, so the
61
+ * user is logged in with `emailVerified: true`.
62
+ */
63
+ declare class Saml {
64
+ private readonly auth;
65
+ private readonly options;
66
+ private readonly providers;
67
+ private readonly clients;
68
+ constructor(auth: Auth, providers: SamlProvider[], options?: SamlOptions);
69
+ names(): string[];
70
+ private lookup;
71
+ /** The IdP redirect URL to start login (SP-initiated). */
72
+ loginUrl(name: string, relayState?: string): Promise<string>;
73
+ /** Validates a posted SAMLResponse and logs the user in by email. */
74
+ consume(name: string, body: {
75
+ SAMLResponse: string;
76
+ RelayState?: string;
77
+ }): Promise<{
78
+ user: PublicUser;
79
+ tokens: TokenPair;
80
+ created: boolean;
81
+ }>;
82
+ /** SP metadata XML (hand this to the IdP admin to register the SP). */
83
+ metadata(name: string): string;
84
+ }
85
+ declare const SAML_SSO: _basaltkit_core.Token<Saml>;
86
+ interface SamlPluginOptions extends SamlOptions {
87
+ providers: SamlProvider[];
88
+ }
89
+ /**
90
+ * Registers the {@link Saml} service (token {@link SAML_SSO}). Adapter-agnostic —
91
+ * the Fastify, Express and Hono adapters all parse the
92
+ * `application/x-www-form-urlencoded` ACS POST. Register it after `authPlugin`.
93
+ */
94
+ declare function samlPlugin(options: SamlPluginOptions): _basaltkit_core.BasaltPlugin<unknown>;
95
+ interface SamlRoutesOptions {
96
+ /**
97
+ * When set, the ACS redirects the browser here after login with
98
+ * `#access_token=…&refresh_token=…`. Omitted → JSON `{ user, accessToken, refreshToken }`.
99
+ */
100
+ successRedirect?: string;
101
+ }
102
+ /**
103
+ * Ready-made SAML routes:
104
+ * - `GET /auth/saml/:provider/login` → 302 to the IdP.
105
+ * - `POST /auth/saml/:provider/acs` → validate the assertion and log in.
106
+ * - `GET /auth/saml/:provider/metadata` → SP metadata XML.
107
+ */
108
+ declare function samlRoutes(options?: SamlRoutesOptions): BasaltRoute[];
109
+
110
+ export { SAML_SSO, Saml, type SamlClient, type SamlOptions, type SamlPluginOptions, type SamlProfile, type SamlProvider, SamlProviderUnknownError, SamlResponseInvalidError, type SamlRoutesOptions, extractEmail, samlPlugin, samlRoutes };