idp-admin 0.1.0 → 0.1.2
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/README.md +79 -2
- package/docs/fake.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Apps that sign people in through an identity provider (Cognito, Keycloak, Entra
|
|
|
6
6
|
|
|
7
7
|
idp-admin is that layer, written once.
|
|
8
8
|
|
|
9
|
-
> **Status:**
|
|
9
|
+
> **Status:** 0.x, so the API may still change between minor versions. The adapters are published and usable directly; `IdpAdmin`, the class that adds the shared rules on top of them (including the access-reduced hook below), isn't built yet. See the [quick start](#quick-start) and the [reference](docs/index.md).
|
|
10
10
|
|
|
11
11
|
## Why it exists
|
|
12
12
|
|
|
@@ -35,9 +35,86 @@ This package came out of exactly that situation: two production apps, each with
|
|
|
35
35
|
- **Not a lowest common denominator.** Providers differ, and some features don't exist everywhere. Adapters declare what they support, and you can always reach the provider's own SDK for anything the shared interface doesn't cover.
|
|
36
36
|
- **Not a UI.** It's the server-side layer an admin screen, a background job or a seed script would call.
|
|
37
37
|
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm install idp-admin
|
|
42
|
+
npm install @aws-sdk/client-cognito-identity-provider # only for Cognito
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Pick an adapter for your provider. Each one's setup (credentials, permissions, realm or pool settings) is in its doc: [Cognito](docs/cognito.md), [Keycloak](docs/keycloak.md), [fake](docs/fake.md).
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import type { IdpAdapter } from "idp-admin"
|
|
49
|
+
import { CognitoAdapter } from "idp-admin/cognito"
|
|
50
|
+
import { KeycloakAdapter } from "idp-admin/keycloak"
|
|
51
|
+
|
|
52
|
+
// Cognito in production, Keycloak in a container locally: the rest of the app doesn't know which
|
|
53
|
+
const idp: IdpAdapter<"company"> = process.env.COGNITO_USER_POOL_ID
|
|
54
|
+
? new CognitoAdapter({
|
|
55
|
+
userPoolId: process.env.COGNITO_USER_POOL_ID,
|
|
56
|
+
region: "us-east-1",
|
|
57
|
+
attributes: { company: "custom:companyName" },
|
|
58
|
+
})
|
|
59
|
+
: new KeycloakAdapter({
|
|
60
|
+
issuer: "http://localhost:8080/realms/acme",
|
|
61
|
+
clientId: "acme-admin",
|
|
62
|
+
clientSecret: process.env.KEYCLOAK_ADMIN_SECRET!,
|
|
63
|
+
attributes: { company: "companyName" },
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`attributes` maps the names your app uses to the provider's own, and only those names compile.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { UserExistsError } from "idp-admin"
|
|
71
|
+
|
|
72
|
+
// create a user who must set their own password at first sign-in; send them the temporary one yourself
|
|
73
|
+
try{
|
|
74
|
+
const user = await idp.createUser({
|
|
75
|
+
email: "dana@example.com",
|
|
76
|
+
name: "Dana Lee",
|
|
77
|
+
attributes: { company: "Acme" },
|
|
78
|
+
temporaryPassword: "Temp-pass-1!",
|
|
79
|
+
})
|
|
80
|
+
if(!(await idp.groupExists("admins"))) await idp.createGroup("admins")
|
|
81
|
+
await idp.addToGroup(user.id, "admins")
|
|
82
|
+
}catch(error){
|
|
83
|
+
if(error instanceof UserExistsError) console.log("already invited")
|
|
84
|
+
else throw error
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// take access away, and end the sessions the provider already issued
|
|
88
|
+
const dana = await idp.findUserByEmail("dana@example.com")
|
|
89
|
+
if(dana){
|
|
90
|
+
await idp.setEnabled(dana.id, false)
|
|
91
|
+
await idp.signOutEverywhere(dana.id)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// list everyone, a page at a time
|
|
95
|
+
let cursor: string | null = null
|
|
96
|
+
do{
|
|
97
|
+
const page = await idp.listUsersPage(cursor)
|
|
98
|
+
for(const user of page.items) console.log(user.email, user.status, user.enabled)
|
|
99
|
+
cursor = page.nextCursor
|
|
100
|
+
}while(cursor)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
A user's `id` is the `sub` in their sign-in tokens, so it matches what your auth library gives you. Pass emails lowercased and trimmed, and `findUserByEmail` returns `null` while `getUser` throws `UserNotFoundError`. The full list of methods, types and errors is in [docs/index.md](docs/index.md).
|
|
104
|
+
|
|
105
|
+
In tests, use the fake instead. It keeps everything in memory and records what a real provider would have done:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { FakeAdapter } from "idp-admin/fake"
|
|
109
|
+
|
|
110
|
+
const fake = new FakeAdapter({ attributes: ["company"] })
|
|
111
|
+
// ...run the code under test with `fake` as its IdpAdapter, then:
|
|
112
|
+
expect(fake.signOuts).toContain(userId)
|
|
113
|
+
```
|
|
114
|
+
|
|
38
115
|
## Providers
|
|
39
116
|
|
|
40
|
-
|
|
117
|
+
Cognito, Keycloak and the fake provider are included today: the ones we run in production and can test properly.
|
|
41
118
|
|
|
42
119
|
The interface is designed so that other providers (Entra ID, Auth0, Okta, Zitadel, Authentik and more) can be added through the same contract. Each one is contributed here and must pass the shared test suite.
|
|
43
120
|
|
package/docs/fake.md
CHANGED
|
@@ -22,7 +22,7 @@ const fake = new FakeAdapter({ attributes: ["company"] })
|
|
|
22
22
|
## Behaviour
|
|
23
23
|
|
|
24
24
|
- **Ids** are UUID v4s from `crypto.randomUUID()`.
|
|
25
|
-
- **Emails** are matched exactly.
|
|
25
|
+
- **Emails** are matched exactly. The adapter contract expects them already lowercased and trimmed, so the fake doesn't lowercase again; pass them that way when calling it directly.
|
|
26
26
|
- **Status:**
|
|
27
27
|
- a user from `createUser` or `createUserWithInvite` starts as `password_change_required`
|
|
28
28
|
- `setPassword` with `temporary: true` sets `password_change_required`, and with `temporary: false` sets `active`
|