@seamless-auth/oidc-provider 0.0.0-stage → 0.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.
Files changed (91) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/LICENSE +201 -0
  3. package/README.md +273 -2
  4. package/dist/adapter.d.ts +44 -0
  5. package/dist/adapter.js +249 -0
  6. package/dist/adapter.js.map +1 -0
  7. package/dist/apiClient.d.ts +35 -0
  8. package/dist/apiClient.js +217 -0
  9. package/dist/apiClient.js.map +1 -0
  10. package/dist/authRequirements.d.ts +37 -0
  11. package/dist/authRequirements.js +58 -0
  12. package/dist/authRequirements.js.map +1 -0
  13. package/dist/clientSecrets.d.ts +10 -0
  14. package/dist/clientSecrets.js +102 -0
  15. package/dist/clientSecrets.js.map +1 -0
  16. package/dist/configCache.d.ts +14 -0
  17. package/dist/configCache.js +45 -0
  18. package/dist/configCache.js.map +1 -0
  19. package/dist/createOidcEdge.d.ts +45 -0
  20. package/dist/createOidcEdge.js +132 -0
  21. package/dist/createOidcEdge.js.map +1 -0
  22. package/dist/deviceFlow.d.ts +53 -0
  23. package/dist/deviceFlow.js +168 -0
  24. package/dist/deviceFlow.js.map +1 -0
  25. package/dist/env.d.ts +16 -0
  26. package/dist/env.js +36 -0
  27. package/dist/env.js.map +1 -0
  28. package/dist/errors.d.ts +18 -0
  29. package/dist/errors.js +40 -0
  30. package/dist/errors.js.map +1 -0
  31. package/dist/firstPartySession.d.ts +21 -0
  32. package/dist/firstPartySession.js +28 -0
  33. package/dist/firstPartySession.js.map +1 -0
  34. package/dist/hostedPages.d.ts +51 -0
  35. package/dist/hostedPages.js +172 -0
  36. package/dist/hostedPages.js.map +1 -0
  37. package/dist/index.d.ts +20 -0
  38. package/dist/index.js +20 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/interactions.d.ts +17 -0
  41. package/dist/interactions.js +209 -0
  42. package/dist/interactions.js.map +1 -0
  43. package/dist/jwtAccessTokens.d.ts +23 -0
  44. package/dist/jwtAccessTokens.js +125 -0
  45. package/dist/jwtAccessTokens.js.map +1 -0
  46. package/dist/logout.d.ts +22 -0
  47. package/dist/logout.js +86 -0
  48. package/dist/logout.js.map +1 -0
  49. package/dist/originGuard.d.ts +10 -0
  50. package/dist/originGuard.js +32 -0
  51. package/dist/originGuard.js.map +1 -0
  52. package/dist/pkce.d.ts +16 -0
  53. package/dist/pkce.js +32 -0
  54. package/dist/pkce.js.map +1 -0
  55. package/dist/providerConfig.d.ts +50 -0
  56. package/dist/providerConfig.js +305 -0
  57. package/dist/providerConfig.js.map +1 -0
  58. package/dist/rateLimit.d.ts +13 -0
  59. package/dist/rateLimit.js +36 -0
  60. package/dist/rateLimit.js.map +1 -0
  61. package/dist/refreshReuse.d.ts +3 -0
  62. package/dist/refreshReuse.js +56 -0
  63. package/dist/refreshReuse.js.map +1 -0
  64. package/dist/requestContext.d.ts +40 -0
  65. package/dist/requestContext.js +43 -0
  66. package/dist/requestContext.js.map +1 -0
  67. package/dist/resourceServers.d.ts +16 -0
  68. package/dist/resourceServers.js +59 -0
  69. package/dist/resourceServers.js.map +1 -0
  70. package/dist/resumeCookie.d.ts +8 -0
  71. package/dist/resumeCookie.js +46 -0
  72. package/dist/resumeCookie.js.map +1 -0
  73. package/dist/server.d.ts +2 -0
  74. package/dist/server.js +36 -0
  75. package/dist/server.js.map +1 -0
  76. package/dist/signingKey.d.ts +29 -0
  77. package/dist/signingKey.js +142 -0
  78. package/dist/signingKey.js.map +1 -0
  79. package/dist/startup.d.ts +4 -0
  80. package/dist/startup.js +19 -0
  81. package/dist/startup.js.map +1 -0
  82. package/dist/tokenAudit.d.ts +4 -0
  83. package/dist/tokenAudit.js +42 -0
  84. package/dist/tokenAudit.js.map +1 -0
  85. package/dist/tokenLifetimes.d.ts +31 -0
  86. package/dist/tokenLifetimes.js +51 -0
  87. package/dist/tokenLifetimes.js.map +1 -0
  88. package/dist/ui/assets/index-CydLXRvo.js +18 -0
  89. package/dist/ui/assets/index-y3LME-Gv.css +1 -0
  90. package/dist/ui/index.html +15 -0
  91. package/package.json +115 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,59 @@
1
+ # @seamless-auth/oidc-provider
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 793a30c: Refresh tokens, authorization codes and device codes are now single use under concurrency too. The API's consume answers whether this request marked the record used (`@seamless-auth/types` 0.34.0). A request that loses gets `invalid_grant`. A lost refresh is handled as refresh token reuse, which ends the first-party session, and a lost code revokes its grant, as a sequential reuse does. A token request whose session ended while it ran now gets `invalid_grant` instead of a server error.
8
+ - dcf17cc: Sign users in to OIDC apps with the authorization code flow and PKCE, discovery and userinfo. Interactions finish only after a first-party sign-in through this service's own adapter, through four JSON routes the hosted pages call: `GET /interaction/:uid/details`, and `POST /interaction/:uid/login`, `/consent` and `/abort`. The provider grant is saved against the first-party session before its auth context is read, and every stored record and signature names that session, so the API's grant binding holds. ID tokens carry the scope claims plus `acr`, `amr` and `auth_time` from the API.
9
+
10
+ - A recovery session cannot finish a sign-in, and `prompt=login` or `max_age` refuse a session older than the request.
11
+ - First-party clients have consent implied. Third-party clients do not exist while `third_party_clients_enabled` is off, and see the consent page on every authorization when it is on.
12
+ - The interaction routes refuse a state change unless `Sec-Fetch-Site` is `same-origin` or the `Origin` is the issuer's, so a sibling subdomain cannot drive them.
13
+
14
+ Needs fells-code/seamless-auth-api#524 for a session signed in to more than one app.
15
+ - 1697942: Client authentication per ADR D6. Confidential clients use `private_key_jwt` by default, and each assertion is single-use through the API's replay store (a replay is `invalid_client`, not a 500). Public clients use `none` with PKCE. `client_secret_basic` and `client_secret_post` are offered only when the API allows client secrets (`allow_client_secrets`, read at startup), and only work for clients opted in on the API.
16
+
17
+ A presented secret is checked with `POST /oidc/clients/:id/verify-secret` before the library runs. When the API accepts it, the request carries a one-time value in its place, and the client's metadata carries that value for that request only, so this service never holds a client's real secret. `client_secret_jwt` and HS* algorithms are never offered.
18
+ - 2532a7c: `pkce.required` is now per client. PKCE stays required unless the API sends `urn:seamless:pkce_required: false` for a confidential client (`@seamless-auth/types` 0.34.0), as RFC 9700 section 2.1.1 allows. Such a client must send `nonce` on any code request without PKCE that asks for an ID token, or gets `invalid_request`, unless the API also sends `urn:seamless:nonce_required: false`. A code request without `openid` still needs PKCE, since a nonce only binds an ID token. An opt-out on a public client is ignored, so PKCE stays required for it.
19
+ - d3b779e: Apply each application's `accessTokenTtlSeconds`, `idTokenTtlSeconds` and `refreshTokenTtlSeconds`, which the API now sends as `urn:seamless:access_token_ttl`, `urn:seamless:id_token_ttl` and `urn:seamless:refresh_token_ttl` client metadata (`@seamless-auth/types` 0.34.0). An access token for a resource takes the shorter of the resource's and the application's lifetime. Unset values keep the defaults (600 seconds for access and ID tokens, 14 days for refresh tokens), and `ttl.RefreshToken` is now set explicitly, keeping the library's rule that a public browser app's rotated token never outlives the one it replaced.
20
+ - 4c74314: The device authorization grant (RFC 8628) for first-party clients with the device flow turned on: `/device/auth` returns an 8-character base-20 user code (about 34.5 bits) that lives 10 minutes and an `interval` of 5 seconds, and `/device` is the verification page in the hosted UI. The page names the app and warns, before any sign-in, to continue only on a device you started yourself. Every approval signs in again, even in a browser that is already signed in. Polling sooner than the interval gets `slow_down` and a longer interval, per device code. Code entry is limited to 10 a minute per address and 600 a minute per instance. Device-flow tokens never carry `acr`, including after a refresh, and `acr_values` and `max_age` are refused on device requests (ADR D10). Sign requests now name the grant (`grantType`), which needs `@seamless-auth/types` 0.34.0 and the API with edge contract version 2.
21
+ - 4d9492a: Hosted login and consent pages. A SPA on `@seamless-auth/react`, served by this service at `/interaction/:uid` and the bundled sign-in paths (`/login`, `/verify-email-otp`, `/register-passkey` and the rest), signs users in through this service's own first-party adapter, so every enabled method and every first-party protection applies. It finishes the interaction once the user is signed in, sends a recovery session to enrollment, and has the user sign in again when the app asks for `prompt=login` or `max_age`.
22
+
23
+ - Third-party clients get a consent page naming the client, the host it sends you back to, and what it asks for.
24
+ - Branding is read from the API's config when a page is served (`app_name` today), with no build step. A config change shows within 30 seconds.
25
+ - Every page gets a strict CSP: scripts from this origin only, styles from this origin or carrying the response's nonce, `frame-ancestors 'none'`, plus `no-store`, `nosniff` and `no-referrer`.
26
+ - The adapter routes under `/auth` now need `Sec-Fetch-Site: same-origin` or the issuer's `Origin` for a state change, like the interaction routes.
27
+ - The resume cookie is now `__Host-oidc_resume` with `Path=/`, and resuming an authorization other than the one it names is refused.
28
+ - The package ships the built pages in `dist/ui`. `createOidcEdge` takes `uiDir` to serve another build.
29
+ - a167b9d: The hosted pages take their branding from the API's `hosted_ui` system config, read from `GET /oidc/config` on each page load like `app_name`. `appName` overrides the name, `logoUrl` shows a logo (its origin is added to the page's `img-src`), `colors` and `fontFamily` become CSS variables in a `<style>` carrying the response's nonce. A branded palette replaces the dark palette, since it was chosen against a light page. The bundled sign-in screens now take their accent, text, surface and border colors from the same variables, so branding reaches every screen. Needs `@seamless-auth/types` with `hostedUi` on the config response.
30
+ - a6de395: Token introspection (RFC 7662) and revocation (RFC 7009) at `/token/introspection` and `/token/revocation`, listed in discovery.
31
+
32
+ - A client may introspect its own tokens. A resource server, registered as a confidential application linked to its resource, may introspect tokens whose `aud` is that resource. Every answer comes from the API's store, so a token whose session has ended is inactive at once.
33
+ - JWT access tokens are now stored under their `jti` when issued, so they can be introspected and revoked. A presented JWT is mapped back to its record only after its signature checks out against the provider's keys, and the `jti` alone is not accepted as a token.
34
+ - Revoking a refresh token ends that app's grant and its tokens, not the user's first-party session. Each revocation is recorded in the API's audit log as `token_revoked`.
35
+ - Both endpoints are limited to 1200 requests a minute per client IP, per instance.
36
+ - a81ad61: First release of the Seamless Auth OpenID Provider service, on `oidc-provider` 9.13 and `@seamless-auth/express`. It stores every provider model through the API's `/oidc/payloads/*` routes, reads clients from the API's registry, and signs every token through `POST /oidc/sign`, so it holds no private key and no database. The configuration is locked to the code flow with PKCE for every client, refresh rotation, no dev interactions and no dynamic registration. At startup it checks `GET /oidc/config` and refuses to run when the API has the provider disabled or speaks a different `/oidc/*` contract version. It answers only requests for the issuer's own origin. Ships as a Docker image and as an npm package that exports `createOidcEdge` for mounting into an Express app.
37
+
38
+ Requires a Seamless Auth API with the v0.21 `/oidc/*` routes and `@seamless-auth/types` 0.34.0.
39
+ - 45ea34c: Refresh token reuse detection and the client credentials grant.
40
+
41
+ - A rotated refresh token presented again still revokes the grant, and now also asks the API to end the first-party session it was issued under (`POST /oidc/sessions/:id/revoke`, reason `refresh_token_reuse`).
42
+ - A refresh reads the session's auth context, so the refreshed ID token carries the current `acr`, `amr` and `auth_time`. Before this, any step-up after sign-in made `/oidc/sign` refuse every later refresh.
43
+ - `client_credentials` is on for clients registered for it. The token is an RFC 9068 JWT for a resource (the one named with `resource`, or the client's only linked one, else `invalid_target`), with `sub` and `client_id` set to the client and scopes cut to what it is allowed there.
44
+ - 30b4115: Issue access tokens for a named API. With `resource=<identifier>` (RFC 8707) the access token is an RFC 9068 JWT (`typ: at+jwt`) whose `aud` is the resource, carrying only the scopes the application is allowed there and the resource's own lifetime, read from the API's `GET /oidc/clients/:id/resources`. A resource the application is not linked to is refused with `invalid_target`. A resource named once at authorization is used at the token endpoint too. Without `resource`, the access token is the same opaque userinfo token as before. First-party apps have resource scopes granted with the sign-in. The consent page lists them for third-party apps.
45
+
46
+ Needs fells-code/seamless-auth-api#533 and `@seamless-auth/types` with `OidcEdgeRoutes.clientResources`.
47
+ - b31200c: RP-initiated logout at `/session/end`, confirmed on a hosted page. Signing out ends the provider session and the first-party session behind it through the API, which sends OpenID Back-Channel Logout tokens to every app the user signed in to with it. Signing out of the asking app only ends that app's grant. Discovery now lists `end_session_endpoint`, `backchannel_logout_supported` and `backchannel_logout_session_supported`, and ID tokens carry `sid` for clients registered for back-channel logout. Needs the API with back-channel logout (fells-code/seamless-auth-api#409).
48
+ - b3048a8: Honour `acr_values`, `max_age` and `prompt=login` from the API's auth context, with step-up in the hosted pages.
49
+
50
+ - Every authorization from an existing provider session reads `GET /oidc/sessions/:id/auth-context` and carries the reported `acr`, `amr` and `auth_time` into the tokens, so a step-up made elsewhere shows up on the next authorization and `/oidc/sign` never sees a stale value. A session the API reports as ended or in recovery goes back to sign-in.
51
+ - `acr_values` is met when the session's acr ranks at least the weakest value asked for (`phrh` satisfies `phr`). `max_age` is met by the API's `auth_time`, or an authentication during the request.
52
+ - `POST /interaction/:uid/login` answers `409 { error: 'step_up_required', methods }` instead of `reauthentication_required`. The pages run the step-up through the adapter (`/step-up/webauthn/*`, or `/totp/verify-mfa` for freshness on a session with no acr), then post again with `{ "afterStepUp": true }`. If the requirement is still not met, the client receives `unmet_authentication_requirements`.
53
+
54
+ Needs fells-code/seamless-auth-api#529.
55
+
56
+ ### Patch Changes
57
+
58
+ - 1397fcd: Document how the provider meets OAuth 2.1 and RFC 9700 in `docs/oauth-security.md`, with the test behind each row, and cover RFC 8252 native clients (loopback redirects on any port, the IPv6 loopback, private-use schemes, and refusal of any other host, path or scheme). No behaviour change.
59
+ - dfd86f2: Depend on `@seamless-auth/types` `^0.34.0`, the stable release, instead of the 0.34.0 prerelease.
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md CHANGED
@@ -1,3 +1,274 @@
1
- # Temporary Holding Version
1
+ # Seamless Auth OpenID Provider
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The OpenID Provider for [Seamless Auth](https://seamlessauth.com). Any app that speaks OpenID
4
+ Connect (Grafana, GitLab, Argo CD, Auth.js and the rest) can sign users in with Seamless Auth
5
+ through this service, with no Seamless SDK.
6
+
7
+ It is the public "login edge" in front of a private Seamless Auth API:
8
+
9
+ ```
10
+ any OIDC app ──► seamless-auth-oidc-provider (public: OIDC endpoints, hosted pages, cookies)
11
+ │ service token, like any adapter
12
+ ▼
13
+ Seamless Auth API (private, no cookies) ── Postgres
14
+ ```
15
+
16
+ - Built on [`oidc-provider`](https://github.com/panva/node-oidc-provider) 9.x.
17
+ - Holds no private keys and no database. The API signs every token after checking its claims,
18
+ and stores all provider state.
19
+ - Code flow with PKCE (a confidential client can be opted out, and then sends `nonce` unless that is waived too), refresh rotation always on, `private_key_jwt` for confidential
20
+ clients, and shared secrets only for clients opted in on the API.
21
+
22
+ The design is in
23
+ [ADR 0001](https://github.com/fells-code/seamless-auth-api/blob/main/docs/adr/0001-openid-provider.md).
24
+
25
+ > **Status:** under construction for Seamless Auth v0.21. Discovery, the authorization code
26
+ > flow with PKCE, userinfo and the hosted login and consent pages work. The remaining grants
27
+ > land in the open [issues](https://github.com/fells-code/seamless-auth-oidc-provider/issues).
28
+
29
+ ## Requirements
30
+
31
+ - Node 22 or newer.
32
+ - A Seamless Auth API with the provider enabled (`oidc_provider_enabled`), `oidc_issuer` set
33
+ to this service's public URL, and the provider signing keys (`OIDC_JWKS_*`) configured.
34
+ - This service's hostname must be the API's `rpid` or a subdomain of it, so passkeys work on the
35
+ hosted pages.
36
+ - HTTPS. Provider cookies are `__Host-` cookies, which browsers only accept over TLS.
37
+
38
+ ## Run the image
39
+
40
+ ```bash
41
+ docker run -p 5314:5314 --env-file .env ghcr.io/fells-code/seamless-auth-oidc-provider:latest
42
+ ```
43
+
44
+ ## Configuration
45
+
46
+ | Variable | Required | Description |
47
+ | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
48
+ | `SEAMLESS_API_URL` | yes | The private API, as this service reaches it. |
49
+ | `SEAMLESS_API_ISSUER` | no | The API's `ISSUER`, when it differs from `SEAMLESS_API_URL`. |
50
+ | `SEAMLESS_OIDC_SERVICE_TOKEN` | yes | This service's own service token secret, `SEAMLESS_OIDC_SERVICE_TOKEN` on the API. It must differ from the adapters' `SEAMLESS_SERVICE_TOKEN`, which this service never holds. At least 32 characters. |
51
+ | `SEAMLESS_JWKS_KID` | no | The `kid` on the service tokens this service signs. |
52
+ | `SEAMLESS_COOKIE_SECRET` | yes | Signs the first-party session cookies. At least 32 characters. |
53
+ | `OIDC_COOKIE_KEYS` | yes | Comma-separated provider cookie keys. The first signs, the rest verify. |
54
+ | `TRUST_PROXY` | no | `true` when TLS ends at a proxy in front of this service. |
55
+ | `PORT` | no | Defaults to `5314`. |
56
+
57
+ The issuer comes from the API (`oidc_issuer`), not from this service's environment. The service
58
+ answers only requests for the issuer's own scheme and host, and refuses to start when the API
59
+ has the provider disabled or speaks a different version of the `/oidc/*` contract.
60
+
61
+ ## Endpoints
62
+
63
+ | Path | What |
64
+ | ----------------------------------- | ------------------------------------------------- |
65
+ | `/.well-known/openid-configuration` | Discovery |
66
+ | `/authorize`, `/token`, `/userinfo` | Authorization code with PKCE, and userinfo |
67
+ | `/jwks` | The provider's public keys, read from the API |
68
+ | `/auth/*` | This service's first-party adapter, for the pages |
69
+ | `GET /interaction/:uid/details` | What the current interaction needs, for the pages |
70
+ | `POST /interaction/:uid/login` | Finishes login, or answers `step_up_required` |
71
+ | `POST /interaction/:uid/consent` | Grants what the client asked for |
72
+ | `POST /interaction/:uid/abort` | Returns `access_denied` to the client |
73
+ | `/session/end` | RP-initiated logout, confirmed on a hosted page |
74
+ | `/device/auth`, `/device` | Device authorization grant and its hosted page |
75
+
76
+ The interaction routes are for the hosted pages only. A state change needs `Sec-Fetch-Site:
77
+ same-origin` or an `Origin` equal to the issuer's, so a page on a sibling subdomain cannot
78
+ drive them.
79
+
80
+ ## Step-up (`acr_values`, `max_age`, `prompt=login`)
81
+
82
+ Discovery lists `acr_values_supported: ["phr", "phrh"]`. `phrh` (a device-bound passkey with
83
+ user verification) satisfies a request for `phr`. On every authorization from an existing
84
+ session this service reads the session's auth context from the API and decides from what the
85
+ API reports, never from its own copy:
86
+
87
+ - `acr_values` is met when the session's acr ranks at least the weakest value asked for.
88
+ - `max_age` is met when the API's `auth_time` is within it, or the user authenticated during
89
+ this request. `prompt=login` needs the latter.
90
+
91
+ When either falls short, the hosted pages ask the user to confirm it is them on the same
92
+ session: a passkey, or an authenticator app code when only freshness is missing on a session
93
+ with no acr (an app code never earns an acr). The pages then read the context again and copy
94
+ `acr`, `amr` and `auth_time` into the tokens unchanged. If the step-up still falls short, for
95
+ example a synced passkey when `phrh` was asked for, the app gets
96
+ `unmet_authentication_requirements` rather than a token with a lower acr.
97
+
98
+ ## Access tokens for an API (`resource`)
99
+
100
+ An app that calls an API of its own asks for a token for it with `resource=<identifier>`
101
+ (RFC 8707), using an identifier the API's registry lists under `/admin/resources` and links to
102
+ the application under `/admin/applications/:id/resources`. The access token is then an RFC
103
+ 9068 JWT (`typ: at+jwt`) whose `aud` is that identifier, carrying `client_id`, `sub`, `scope`
104
+ and `jti`, with only the scopes the application is allowed there and the resource's own
105
+ lifetime. The API checks the same links again before it signs, so an API configured for one
106
+ identifier can reject every token minted for another.
107
+
108
+ - A resource the application is not linked to is refused with `invalid_target`.
109
+ - An app that named one resource at authorization gets its token without repeating
110
+ `resource` at the token endpoint.
111
+ - Without `resource` the access token stays an opaque one for `/userinfo`, as before. A token
112
+ for a resource is not accepted at `/userinfo`.
113
+ - First-party apps have the scopes granted with the sign-in. Third-party apps list them on the
114
+ consent page.
115
+
116
+ ## Token lifetimes
117
+
118
+ An application's own `accessTokenTtlSeconds`, `idTokenTtlSeconds` and `refreshTokenTtlSeconds`
119
+ (admin routes) apply to its tokens. Unset ones use the defaults: 600 seconds for access and ID
120
+ tokens, 14 days for refresh tokens. An access token for a resource with its own lifetime gets
121
+ the shorter of the two. A rotated refresh token for a browser app that cannot bind it to a key
122
+ keeps what was left of the one it replaced, whatever the application's lifetime. The grant
123
+ behind them lives at least as long as the application's refresh tokens. The API also
124
+ ends every record when the first-party session behind it ends.
125
+
126
+ ## Refresh tokens and client credentials
127
+
128
+ - **Refresh.** A client registered for `refresh_token` gets one with `offline_access`, which
129
+ the library honours only on a request with `prompt=consent` (OpenID Connect Core 11). Refresh
130
+ tokens rotate on every use. A used one presented again revokes the grant and every token
131
+ under it, and the API ends the first-party session it was issued under. Two refreshes
132
+ racing with the same token cannot both rotate: the API's consume says which one marked it
133
+ used, and the other is handled as that same reuse. A refreshed ID
134
+ token carries the session's current `acr`, `amr` and `auth_time`, so a step-up since the
135
+ sign-in shows up on the next refresh.
136
+ - **Client credentials.** A confidential client registered for `client_credentials` gets an
137
+ RFC 9068 JWT whose `sub` and `client_id` are the client itself, only ever for a resource:
138
+ the one named with `resource`, or the client's only linked resource. Scopes are cut to what
139
+ the client is allowed there. An application with account notices turned on also gets the
140
+ built-in `<issuer>/seamless` resource with `notices:send`, which the API lists for it, for
141
+ `POST /applications/account-notices` on the API.
142
+
143
+ ## Introspection and revocation
144
+
145
+ `/token/introspection` (RFC 7662) and `/token/revocation` (RFC 7009) are listed in discovery
146
+ and take the same client authentication as `/token`.
147
+
148
+ - **Introspection.** A client may introspect its own tokens. A resource server registers as a
149
+ confidential application linked to its resource, and may then introspect tokens whose `aud`
150
+ is that resource. Any other caller gets `{ "active": false }`. Every answer is read from the
151
+ API's store, so a token whose first-party session has ended is inactive at once.
152
+ - **JWT access tokens.** The library keeps no record of a JWT, so this service stores one
153
+ under its `jti` when it is issued and maps a presented JWT back to it, after checking the
154
+ signature against the provider's own keys. The `jti` alone is never accepted as a token.
155
+ A resource server that verifies JWTs locally still accepts one until it expires; one that
156
+ introspects sees revocation immediately.
157
+ - **Revocation.** A client revokes its own refresh or access token. Revoking a refresh token
158
+ ends the grant and every token under it, not the user's first-party session, so the user's
159
+ other apps stay signed in. Each revocation is recorded in the API's audit log
160
+ (`token_revoked`).
161
+ - **Rate limit.** 1200 requests a minute per client IP across both endpoints, per instance.
162
+
163
+ ## Logout
164
+
165
+ `/session/end` (OpenID RP-Initiated Logout 1.0) takes `id_token_hint` or `client_id`, and a
166
+ `post_logout_redirect_uri` registered for the client. The hosted page asks before anything
167
+ ends:
168
+
169
+ - **Sign out** ends the provider session and the first-party session it was made under
170
+ (through the API's `/oidc/sessions/:id/revoke`, reason `provider_logout`), and clears the
171
+ adapter cookies. Every other app the user signed in to with it is signed out too.
172
+ - **Only sign out of** the asking app ends that app's grant and keeps the session.
173
+
174
+ Back-channel logout (OpenID Back-Channel Logout 1.0) is sent by the API, not by this service.
175
+ The API holds the keys and sees every way a session ends (this logout, admin revoke, sign-out
176
+ everywhere, eviction), so a client with a `backchannelLogoutUri` gets a `logout+jwt` token
177
+ whichever one it was. Discovery lists `backchannel_logout_supported` and
178
+ `backchannel_logout_session_supported`, and ID tokens for such a client carry `sid`.
179
+
180
+ ## Device authorization grant
181
+
182
+ `/device/auth` (RFC 8628) is for apps that cannot open a browser: TVs, consoles, CLIs. It is
183
+ off unless an admin enables the device flow on a first-party application (ADR D10).
184
+
185
+ - **Codes.** User codes are 8 characters from the RFC's base-20 set (`BCDF-GHJK`), about 34.5
186
+ bits, typed case-insensitively and with or without the dash. They live 10 minutes. The
187
+ device code works once.
188
+ - **Polling.** The response carries `interval: 5`. A poll sooner than that gets `slow_down`
189
+ and five more seconds, tracked per device code on each instance.
190
+ - **Approval.** `/device` is a hosted page. Before any sign-in, the confirmation page names the
191
+ app, shows the code, and says to continue only on a device you started yourself (RFC 8628
192
+ section 5.4). Every approval signs in again, so a browser that is already signed in still
193
+ confirms with the passkey. Code entry is limited to 10 a minute per address and 600 a
194
+ minute per instance.
195
+ - **Assurance.** Device-flow tokens never carry `acr`, after a refresh too, and device requests
196
+ with `acr_values` or `max_age` are refused. The passkey proves who approved, not who holds
197
+ the device. The API refuses `acr` on any token whose sign request names the device grant.
198
+
199
+ ## Security defaults
200
+
201
+ The configuration is locked to OAuth 2.1 and RFC 9700: the code flow with `S256` PKCE for
202
+ every client (unless an admin opts a confidential client out, RFC 9700 section 2.1.1, in
203
+ which case it must send `nonce` whenever it asks for an ID token, unless an admin waives
204
+ that as well), exact redirect matching (with RFC 8252 loopback ports for native apps), refresh
205
+ rotation, `iss` on authorization responses and audience-restricted access tokens. The full
206
+ matrix, with the test behind each row, is in [docs/oauth-security.md](docs/oauth-security.md).
207
+
208
+ ## Client authentication
209
+
210
+ | Client | Authentication |
211
+ | ----------------------------------------- | --------------------------------------------- |
212
+ | Confidential (server-side apps, services) | `private_key_jwt`, each assertion used once |
213
+ | Public (SPA, native, CLI) | `none`, with PKCE |
214
+ | Confidential, opted in to a shared secret | `client_secret_basic` or `client_secret_post` |
215
+
216
+ Shared secrets are for apps that support nothing else. They need `allow_client_secrets` on
217
+ the API (read when this service starts) and a per-client opt-in there. This service never
218
+ holds a client's secret: it asks the API to check a presented one, then hands the library a
219
+ one-time stand-in for that request. `client_secret_jwt` and HS\* algorithms are never offered.
220
+
221
+ ## Hosted pages
222
+
223
+ The login and consent pages are a SPA in `ui/`, built on `@seamless-auth/react` and shipped
224
+ in `dist/ui`. They sign users in through this service's own adapter routes (`/auth/*`), the
225
+ same way any Seamless app does, then finish the OIDC interaction.
226
+
227
+ - **Methods.** Every method the API has enabled in `login_methods`, through the SDK's bundled
228
+ screens. A recovery session goes to enrollment before it can finish a sign-in.
229
+ - **Consent.** First-party clients have consent implied. Third-party clients see the client
230
+ name, the host they are sent back to and the requested scopes, on every authorization.
231
+ - **Branding.** The app name comes from the API's config each time a page is served, so a
232
+ config change needs no build and no restart.
233
+ - **Headers.** A strict CSP (`script-src 'self'`, styles from this origin or with the
234
+ response's nonce, `frame-ancestors 'none'`), `no-store`, `nosniff` and `no-referrer`.
235
+
236
+ Every cookie this service sets is `__Host-` (host-only, `Secure`, `Path=/`), so serve it over
237
+ HTTPS, including in development: the library refuses to set a `Secure` cookie on a plain
238
+ HTTP request.
239
+
240
+ ## Embed in an Express app
241
+
242
+ ```ts
243
+ import express from 'express';
244
+ import { createOidcEdge } from '@seamless-auth/oidc-provider';
245
+
246
+ const edge = await createOidcEdge({
247
+ apiUrl: process.env.SEAMLESS_API_URL!,
248
+ serviceSecret: process.env.SEAMLESS_OIDC_SERVICE_TOKEN!,
249
+ cookieKeys: process.env.OIDC_COOKIE_KEYS!.split(','),
250
+ adapterCookieSecret: process.env.SEAMLESS_COOKIE_SECRET!,
251
+ trustProxy: true,
252
+ });
253
+
254
+ const app = express();
255
+ app.set('trust proxy', 1);
256
+ app.use(edge.router);
257
+ app.listen(5314);
258
+ ```
259
+
260
+ Mount `edge.router` at the root of the app that serves the issuer's origin.
261
+
262
+ ## Development
263
+
264
+ ```bash
265
+ nvm use
266
+ npm ci
267
+ npm run typecheck && npm run lint && npm run format:check && npm run coverage && npm run build
268
+ ```
269
+
270
+ See [AGENTS.md](AGENTS.md) for the repo's standards and [RELEASES.md](RELEASES.md) for releases.
271
+
272
+ ## License
273
+
274
+ Apache-2.0. See [LICENSE](LICENSE).
@@ -0,0 +1,44 @@
1
+ import type { Adapter, AdapterFactory, AdapterPayload } from 'oidc-provider';
2
+ import { errors } from 'oidc-provider';
3
+ import type { OidcApiClient } from './apiClient.js';
4
+ import type { EdgeConfigCache } from './configCache.js';
5
+ import { DevicePollThrottle } from './deviceFlow.js';
6
+ /** A client assertion or proof presented twice. Answered as `invalid_client`, not a 500. */
7
+ export declare class ReplayDetectedError extends errors.InvalidClientAuth {
8
+ constructor();
9
+ }
10
+ export declare class UnsupportedModelError extends Error {
11
+ constructor(name: string, operation: string);
12
+ }
13
+ /** Stores one library model through the API's `/oidc/payloads/*` routes. */
14
+ export declare class ApiPayloadAdapter implements Adapter {
15
+ #private;
16
+ constructor(name: string, client: OidcApiClient, polls?: DevicePollThrottle);
17
+ upsert(id: string, payload: AdapterPayload, expiresIn?: number): Promise<void>;
18
+ find(id: string): Promise<AdapterPayload | undefined>;
19
+ findByUserCode(userCode: string): Promise<AdapterPayload | undefined>;
20
+ findByUid(uid: string): Promise<AdapterPayload | undefined>;
21
+ consume(id: string): Promise<void>;
22
+ destroy(id: string): Promise<void>;
23
+ revokeByGrantId(grantId: string): Promise<void>;
24
+ }
25
+ /**
26
+ * Reads clients from the API's registry. Clients are managed there, never here. A third-party
27
+ * client does not exist while `third_party_clients_enabled` is off (ADR D10).
28
+ *
29
+ * The registry never returns a secret. A client on a shared-secret method gets the one-time
30
+ * value `verifyClientSecrets` set for a secret the API accepted on this request, and otherwise
31
+ * a random value nothing can present, which the library needs to load the client at all.
32
+ */
33
+ export declare class ApiClientAdapter implements Adapter {
34
+ #private;
35
+ constructor(client: OidcApiClient, config: EdgeConfigCache);
36
+ find(id: string): Promise<AdapterPayload | undefined>;
37
+ upsert(): Promise<void>;
38
+ findByUserCode(): Promise<undefined>;
39
+ findByUid(): Promise<undefined>;
40
+ consume(): Promise<void>;
41
+ destroy(): Promise<void>;
42
+ revokeByGrantId(): Promise<void>;
43
+ }
44
+ export declare function createApiAdapterFactory(client: OidcApiClient, config: EdgeConfigCache): AdapterFactory;