@wtfalch/auth 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.
- package/dist/auth.d.ts +136 -0
- package/dist/auth.js +403 -0
- package/dist/broker.d.ts +63 -0
- package/dist/broker.js +102 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.js +71 -0
- package/dist/cookies.d.ts +45 -0
- package/dist/cookies.js +131 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/next.d.ts +68 -0
- package/dist/next.js +111 -0
- package/dist/oidc.d.ts +35 -0
- package/dist/oidc.js +128 -0
- package/dist/redirect.d.ts +2 -0
- package/dist/redirect.js +16 -0
- package/package.json +54 -0
package/dist/broker.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export class BrokerError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
error;
|
|
4
|
+
detail;
|
|
5
|
+
constructor(status, error, detail) {
|
|
6
|
+
super(detail ?? error);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.error = error;
|
|
9
|
+
this.detail = detail;
|
|
10
|
+
this.name = 'BrokerError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The privileged half, over HTTP. The app holds a key that reaches its own
|
|
15
|
+
* organisation through these calls only; the token that can sign anyone in
|
|
16
|
+
* anywhere stays in the service.
|
|
17
|
+
*/
|
|
18
|
+
export class Broker {
|
|
19
|
+
options;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
}
|
|
23
|
+
async call(method, path, body) {
|
|
24
|
+
if (!this.options.appKey) {
|
|
25
|
+
throw new Error('@wtfalch/auth: appKey is required to sign people in on the app');
|
|
26
|
+
}
|
|
27
|
+
const url = new URL(`${this.options.brokerUrl}${path}`);
|
|
28
|
+
if (method === 'GET')
|
|
29
|
+
for (const [k, v] of Object.entries(body))
|
|
30
|
+
url.searchParams.set(k, String(v));
|
|
31
|
+
const response = await this.options.fetch(url.href, {
|
|
32
|
+
method,
|
|
33
|
+
headers: {
|
|
34
|
+
authorization: `Bearer ${this.options.appKey}`,
|
|
35
|
+
'content-type': 'application/json',
|
|
36
|
+
},
|
|
37
|
+
body: method === 'GET'
|
|
38
|
+
? undefined
|
|
39
|
+
: JSON.stringify({ ...body, origin: this.options.appUrl.origin }),
|
|
40
|
+
});
|
|
41
|
+
const text = await response.text();
|
|
42
|
+
let data = {};
|
|
43
|
+
try {
|
|
44
|
+
data = text ? JSON.parse(text) : {};
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new BrokerError(response.status, 'unavailable', `non-JSON response from ${path}`);
|
|
48
|
+
}
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new BrokerError(response.status, String(data.error ?? 'unavailable'), data.message);
|
|
51
|
+
}
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
authRequest(id) {
|
|
55
|
+
return this.call('GET', '/auth-request', { id });
|
|
56
|
+
}
|
|
57
|
+
async signIn(input) {
|
|
58
|
+
const { callbackUrl } = await this.call('POST', '/sign-in', input);
|
|
59
|
+
return callbackUrl;
|
|
60
|
+
}
|
|
61
|
+
async signUp(input) {
|
|
62
|
+
const { callbackUrl } = await this.call('POST', '/sign-up', {
|
|
63
|
+
...input,
|
|
64
|
+
});
|
|
65
|
+
return callbackUrl;
|
|
66
|
+
}
|
|
67
|
+
async followLink(input) {
|
|
68
|
+
const { callbackUrl } = await this.call('POST', '/link/follow', input);
|
|
69
|
+
return callbackUrl;
|
|
70
|
+
}
|
|
71
|
+
async completeReset(input) {
|
|
72
|
+
const { callbackUrl } = await this.call('POST', '/reset/complete', input);
|
|
73
|
+
return callbackUrl;
|
|
74
|
+
}
|
|
75
|
+
invite(input) {
|
|
76
|
+
return this.call('POST', '/invite', input);
|
|
77
|
+
}
|
|
78
|
+
sendLink(email, next) {
|
|
79
|
+
return this.call('POST', '/link/send', { email, next });
|
|
80
|
+
}
|
|
81
|
+
sendReset(email, next) {
|
|
82
|
+
return this.call('POST', '/reset/send', { email, next });
|
|
83
|
+
}
|
|
84
|
+
sendVerification(email, next) {
|
|
85
|
+
return this.call('POST', '/verification/send', { email, next });
|
|
86
|
+
}
|
|
87
|
+
verifyEmail(userId, code) {
|
|
88
|
+
return this.call('POST', '/verify', { userId, code });
|
|
89
|
+
}
|
|
90
|
+
/** A public client revoking its own token: no key, no service. */
|
|
91
|
+
async revoke(refreshToken) {
|
|
92
|
+
await this.options.fetch(`${this.options.issuer}/oauth/v2/revoke`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
95
|
+
body: new URLSearchParams({
|
|
96
|
+
token: refreshToken,
|
|
97
|
+
token_type_hint: 'refresh_token',
|
|
98
|
+
client_id: this.options.clientId,
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface AuthOptions {
|
|
2
|
+
/** This app's origin, e.g. https://portal.valet.wtfalch.dev. Every URL the SDK builds starts here, never from the request's Host. */
|
|
3
|
+
appUrl: string | undefined;
|
|
4
|
+
/** From scripts/provisioned.json. */
|
|
5
|
+
clientId: string | undefined;
|
|
6
|
+
/** From scripts/provisioned.json. Sent as a scope and checked on every token. */
|
|
7
|
+
organizationId: string | undefined;
|
|
8
|
+
/** 32 bytes, base64 or hex: `openssl rand -base64 32`. One per app. */
|
|
9
|
+
cookieSecret: string | undefined;
|
|
10
|
+
/** Default https://auth.wtfalch.dev. */
|
|
11
|
+
issuer?: string;
|
|
12
|
+
/** Where the routes are mounted. Default /auth. */
|
|
13
|
+
basePath?: string;
|
|
14
|
+
/** Default /. */
|
|
15
|
+
afterLogin?: string;
|
|
16
|
+
/** Default /. Must be a registered post-logout URI. */
|
|
17
|
+
afterLogout?: string;
|
|
18
|
+
/** Where a failed callback lands, with ?auth_error=<reason>. Default /. */
|
|
19
|
+
onError?: string;
|
|
20
|
+
/** Where a verified email link lands. Default /. */
|
|
21
|
+
afterVerify?: string;
|
|
22
|
+
/** This app's key for the sign-in service (APP_KEY_<name>). Needed for anything that checks a credential. */
|
|
23
|
+
appKey?: string;
|
|
24
|
+
/** The sign-in service. Defaults to the issuer's /api. */
|
|
25
|
+
brokerUrl?: string;
|
|
26
|
+
/** Cookie lifetime in seconds, sliding. Default 30 days, the issuer's idle expiry for a refresh token. */
|
|
27
|
+
sessionMaxAge?: number;
|
|
28
|
+
/** Refresh when the id token has fewer seconds left than this. Default 300. */
|
|
29
|
+
refreshWindow?: number;
|
|
30
|
+
fetch?: typeof fetch;
|
|
31
|
+
}
|
|
32
|
+
export interface ResolvedOptions {
|
|
33
|
+
appUrl: URL;
|
|
34
|
+
clientId: string;
|
|
35
|
+
organizationId: string;
|
|
36
|
+
cookieKey: Uint8Array;
|
|
37
|
+
issuer: string;
|
|
38
|
+
basePath: string;
|
|
39
|
+
afterLogin: string;
|
|
40
|
+
afterLogout: string;
|
|
41
|
+
onError: string;
|
|
42
|
+
afterVerify: string;
|
|
43
|
+
appKey: string | null;
|
|
44
|
+
brokerUrl: string;
|
|
45
|
+
sessionMaxAge: number;
|
|
46
|
+
refreshWindow: number;
|
|
47
|
+
secure: boolean;
|
|
48
|
+
fetch: typeof fetch;
|
|
49
|
+
}
|
|
50
|
+
export declare const ISSUER = "https://auth.wtfalch.dev";
|
|
51
|
+
export declare function resolveOptions(options: AuthOptions): ResolvedOptions;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const ISSUER = 'https://auth.wtfalch.dev';
|
|
2
|
+
export function resolveOptions(options) {
|
|
3
|
+
const appUrl = parseAppUrl(options.appUrl);
|
|
4
|
+
required(options.clientId, 'clientId');
|
|
5
|
+
required(options.organizationId, 'organizationId');
|
|
6
|
+
const basePath = options.basePath ?? '/auth';
|
|
7
|
+
if (!basePath.startsWith('/') || basePath.endsWith('/')) {
|
|
8
|
+
throw new Error(`@wtfalch/auth: basePath must start with "/" and not end with one, got "${basePath}"`);
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
appUrl,
|
|
12
|
+
clientId: options.clientId,
|
|
13
|
+
organizationId: options.organizationId,
|
|
14
|
+
cookieKey: decodeKey(options.cookieSecret),
|
|
15
|
+
issuer: (options.issuer ?? ISSUER).replace(/\/$/, ''),
|
|
16
|
+
basePath,
|
|
17
|
+
afterLogin: options.afterLogin ?? '/',
|
|
18
|
+
afterLogout: options.afterLogout ?? '/',
|
|
19
|
+
onError: options.onError ?? '/',
|
|
20
|
+
afterVerify: options.afterVerify ?? '/',
|
|
21
|
+
appKey: options.appKey || null,
|
|
22
|
+
brokerUrl: (options.brokerUrl ?? `${(options.issuer ?? ISSUER).replace(/\/$/, '')}/api`).replace(/\/$/, ''),
|
|
23
|
+
sessionMaxAge: seconds(options.sessionMaxAge, 'sessionMaxAge', 30 * 24 * 60 * 60),
|
|
24
|
+
refreshWindow: seconds(options.refreshWindow, 'refreshWindow', 5 * 60),
|
|
25
|
+
secure: appUrl.protocol === 'https:',
|
|
26
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function required(value, name) {
|
|
30
|
+
if (!value)
|
|
31
|
+
throw new Error(`@wtfalch/auth: ${name} is required`);
|
|
32
|
+
}
|
|
33
|
+
function seconds(value, name, fallback) {
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
return fallback;
|
|
36
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
37
|
+
throw new Error(`@wtfalch/auth: ${name} must be a positive number of seconds, got ${value}`);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function parseAppUrl(raw) {
|
|
42
|
+
required(raw, 'appUrl');
|
|
43
|
+
let url;
|
|
44
|
+
try {
|
|
45
|
+
url = new URL(raw);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error(`@wtfalch/auth: appUrl must be an absolute URL, got "${raw}"`);
|
|
49
|
+
}
|
|
50
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
51
|
+
throw new Error(`@wtfalch/auth: appUrl must be http(s), got "${raw}"`);
|
|
52
|
+
}
|
|
53
|
+
if (url.pathname !== '/' || url.search || url.hash) {
|
|
54
|
+
throw new Error(`@wtfalch/auth: appUrl is an origin, not a page: "${raw}"`);
|
|
55
|
+
}
|
|
56
|
+
return url;
|
|
57
|
+
}
|
|
58
|
+
function decodeKey(secret) {
|
|
59
|
+
required(secret, 'cookieSecret');
|
|
60
|
+
let bytes = new Uint8Array(0);
|
|
61
|
+
try {
|
|
62
|
+
bytes = /^[0-9a-f]{64}$/i.test(secret)
|
|
63
|
+
? Uint8Array.from(secret.match(/../g) ?? [], (pair) => Number.parseInt(pair, 16))
|
|
64
|
+
: Uint8Array.from(atob(secret.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));
|
|
65
|
+
}
|
|
66
|
+
catch { }
|
|
67
|
+
if (bytes.length !== 32) {
|
|
68
|
+
throw new Error(`@wtfalch/auth: cookieSecret must decode to 32 bytes (openssl rand -base64 32), got ${bytes.length}`);
|
|
69
|
+
}
|
|
70
|
+
return bytes;
|
|
71
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type JWTPayload } from 'jose';
|
|
2
|
+
import type { ResolvedOptions } from './config.js';
|
|
3
|
+
export interface SessionPayload extends JWTPayload {
|
|
4
|
+
idt: string;
|
|
5
|
+
rt?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TransactionPayload extends JWTPayload {
|
|
8
|
+
st: string;
|
|
9
|
+
nc: string;
|
|
10
|
+
cv: string;
|
|
11
|
+
nx: string;
|
|
12
|
+
}
|
|
13
|
+
export interface CookieAttributes {
|
|
14
|
+
path: '/';
|
|
15
|
+
httpOnly: true;
|
|
16
|
+
sameSite: 'lax';
|
|
17
|
+
secure: boolean;
|
|
18
|
+
maxAge: number;
|
|
19
|
+
}
|
|
20
|
+
export interface LinkPayload extends JWTPayload {
|
|
21
|
+
sid: string;
|
|
22
|
+
stk: string;
|
|
23
|
+
ar: string;
|
|
24
|
+
}
|
|
25
|
+
export interface SetCookie {
|
|
26
|
+
name: string;
|
|
27
|
+
value: string;
|
|
28
|
+
header: string;
|
|
29
|
+
attributes: CookieAttributes;
|
|
30
|
+
}
|
|
31
|
+
export declare function sessionCookieName(options: ResolvedOptions): string;
|
|
32
|
+
export declare function transactionCookieName(options: ResolvedOptions): string;
|
|
33
|
+
export declare function sealSession(options: ResolvedOptions, payload: Omit<SessionPayload, keyof JWTPayload>): Promise<SetCookie>;
|
|
34
|
+
export declare function openSession(options: ResolvedOptions, value: string | undefined): Promise<SessionPayload | null>;
|
|
35
|
+
export declare function clearSession(options: ResolvedOptions): SetCookie;
|
|
36
|
+
export declare function sealTransaction(options: ResolvedOptions, payload: Omit<TransactionPayload, keyof JWTPayload>): Promise<SetCookie>;
|
|
37
|
+
export declare function openTransaction(options: ResolvedOptions, value: string | undefined): Promise<TransactionPayload | null>;
|
|
38
|
+
export declare function clearTransaction(options: ResolvedOptions): SetCookie;
|
|
39
|
+
export declare function linkCookieName(options: ResolvedOptions): string;
|
|
40
|
+
export declare function sealLink(options: ResolvedOptions, payload: Omit<LinkPayload, keyof JWTPayload>): Promise<SetCookie>;
|
|
41
|
+
export declare function openLink(options: ResolvedOptions, value: string | undefined): Promise<LinkPayload | null>;
|
|
42
|
+
export declare function clearLink(options: ResolvedOptions): SetCookie;
|
|
43
|
+
export declare function cookieFrom(header: string | null, name: string): string | undefined;
|
|
44
|
+
/** The Cookie header with these cookies replaced or removed, for a proxy to forward. */
|
|
45
|
+
export declare function withCookies(header: string | null, updates: SetCookie[]): string;
|
package/dist/cookies.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { EncryptJWT, jwtDecrypt } from 'jose';
|
|
2
|
+
import { AuthError } from './oidc.js';
|
|
3
|
+
const SESSION = 'wtfalch_auth';
|
|
4
|
+
const TRANSACTION = 'wtfalch_auth_tx';
|
|
5
|
+
const LINK = 'wtfalch_auth_link';
|
|
6
|
+
const TRANSACTION_TTL = 10 * 60;
|
|
7
|
+
export function sessionCookieName(options) {
|
|
8
|
+
return options.secure ? `__Host-${SESSION}` : SESSION;
|
|
9
|
+
}
|
|
10
|
+
export function transactionCookieName(options) {
|
|
11
|
+
return options.secure ? `__Host-${TRANSACTION}` : TRANSACTION;
|
|
12
|
+
}
|
|
13
|
+
export async function sealSession(options, payload) {
|
|
14
|
+
const value = await seal(options, 'session', payload, options.sessionMaxAge);
|
|
15
|
+
const cookie = setCookie(sessionCookieName(options), value, options.sessionMaxAge, options.secure);
|
|
16
|
+
if (cookie.header.length > 4096) {
|
|
17
|
+
throw new AuthError('cookie', `the session cookie is ${cookie.header.length} bytes; browsers drop over 4096`);
|
|
18
|
+
}
|
|
19
|
+
return cookie;
|
|
20
|
+
}
|
|
21
|
+
export async function openSession(options, value) {
|
|
22
|
+
const payload = await open(options, 'session', value);
|
|
23
|
+
return payload && typeof payload.idt === 'string' ? payload : null;
|
|
24
|
+
}
|
|
25
|
+
export function clearSession(options) {
|
|
26
|
+
return setCookie(sessionCookieName(options), '', 0, options.secure);
|
|
27
|
+
}
|
|
28
|
+
export async function sealTransaction(options, payload) {
|
|
29
|
+
const value = await seal(options, 'transaction', payload, TRANSACTION_TTL);
|
|
30
|
+
return setCookie(transactionCookieName(options), value, TRANSACTION_TTL, options.secure);
|
|
31
|
+
}
|
|
32
|
+
export async function openTransaction(options, value) {
|
|
33
|
+
const payload = await open(options, 'transaction', value);
|
|
34
|
+
return payload &&
|
|
35
|
+
typeof payload.st === 'string' &&
|
|
36
|
+
typeof payload.nc === 'string' &&
|
|
37
|
+
typeof payload.cv === 'string' &&
|
|
38
|
+
typeof payload.nx === 'string'
|
|
39
|
+
? payload
|
|
40
|
+
: null;
|
|
41
|
+
}
|
|
42
|
+
export function clearTransaction(options) {
|
|
43
|
+
return setCookie(transactionCookieName(options), '', 0, options.secure);
|
|
44
|
+
}
|
|
45
|
+
export function linkCookieName(options) {
|
|
46
|
+
return options.secure ? `__Host-${LINK}` : LINK;
|
|
47
|
+
}
|
|
48
|
+
export async function sealLink(options, payload) {
|
|
49
|
+
const value = await seal(options, 'link', payload, TRANSACTION_TTL);
|
|
50
|
+
return setCookie(linkCookieName(options), value, TRANSACTION_TTL, options.secure);
|
|
51
|
+
}
|
|
52
|
+
export async function openLink(options, value) {
|
|
53
|
+
const payload = await open(options, 'link', value);
|
|
54
|
+
return payload &&
|
|
55
|
+
typeof payload.sid === 'string' &&
|
|
56
|
+
typeof payload.stk === 'string' &&
|
|
57
|
+
typeof payload.ar === 'string'
|
|
58
|
+
? payload
|
|
59
|
+
: null;
|
|
60
|
+
}
|
|
61
|
+
export function clearLink(options) {
|
|
62
|
+
return setCookie(linkCookieName(options), '', 0, options.secure);
|
|
63
|
+
}
|
|
64
|
+
async function seal(options, kind, payload, ttl) {
|
|
65
|
+
return new EncryptJWT(payload)
|
|
66
|
+
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
|
|
67
|
+
.setIssuer(options.appUrl.origin)
|
|
68
|
+
.setAudience(kind)
|
|
69
|
+
.setIssuedAt()
|
|
70
|
+
.setExpirationTime(Math.floor(Date.now() / 1000) + ttl)
|
|
71
|
+
.encrypt(options.cookieKey);
|
|
72
|
+
}
|
|
73
|
+
async function open(options, kind, value) {
|
|
74
|
+
if (!value)
|
|
75
|
+
return null;
|
|
76
|
+
try {
|
|
77
|
+
const { payload } = await jwtDecrypt(value, options.cookieKey, {
|
|
78
|
+
issuer: options.appUrl.origin,
|
|
79
|
+
audience: kind,
|
|
80
|
+
contentEncryptionAlgorithms: ['A256GCM'],
|
|
81
|
+
keyManagementAlgorithms: ['dir'],
|
|
82
|
+
});
|
|
83
|
+
return payload;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function setCookie(name, value, maxAge, secure) {
|
|
90
|
+
const parts = [`${name}=${value}`, 'Path=/', 'HttpOnly', 'SameSite=Lax'];
|
|
91
|
+
if (maxAge > 0) {
|
|
92
|
+
parts.push(`Max-Age=${maxAge}`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
parts.push('Max-Age=0', 'Expires=Thu, 01 Jan 1970 00:00:00 GMT');
|
|
96
|
+
}
|
|
97
|
+
if (secure)
|
|
98
|
+
parts.push('Secure');
|
|
99
|
+
return {
|
|
100
|
+
name,
|
|
101
|
+
value,
|
|
102
|
+
header: parts.join('; '),
|
|
103
|
+
attributes: { path: '/', httpOnly: true, sameSite: 'lax', secure, maxAge },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function cookieFrom(header, name) {
|
|
107
|
+
for (const part of (header ?? '').split(';')) {
|
|
108
|
+
const eq = part.indexOf('=');
|
|
109
|
+
if (eq !== -1 && part.slice(0, eq).trim() === name) {
|
|
110
|
+
const value = part.slice(eq + 1).trim();
|
|
111
|
+
return value.length > 0 ? value : undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
/** The Cookie header with these cookies replaced or removed, for a proxy to forward. */
|
|
117
|
+
export function withCookies(header, updates) {
|
|
118
|
+
const names = new Set(updates.map((u) => u.name));
|
|
119
|
+
const kept = (header ?? '')
|
|
120
|
+
.split(';')
|
|
121
|
+
.map((part) => part.trim())
|
|
122
|
+
.filter((part) => part.length > 0)
|
|
123
|
+
.filter((part) => {
|
|
124
|
+
const eq = part.indexOf('=');
|
|
125
|
+
return eq === -1 || !names.has(part.slice(0, eq).trim());
|
|
126
|
+
});
|
|
127
|
+
for (const u of updates)
|
|
128
|
+
if (u.value)
|
|
129
|
+
kept.push(`${u.name}=${u.value}`);
|
|
130
|
+
return kept.join('; ');
|
|
131
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { type Auth, type AuthRequest, type Gate, type GateRules, type Intent, type NewUser, type ReadResult, type ResetError, type SignInError, type SignInResult, type SignUpError, type SignUpResult, type SignedIn, type User, createAuth, } from './auth.js';
|
|
2
|
+
export { type AuthOptions, ISSUER } from './config.js';
|
|
3
|
+
export type { SetCookie } from './cookies.js';
|
|
4
|
+
export { BrokerError } from './broker.js';
|
|
5
|
+
export { AuthError, type AuthErrorReason, ORG_CLAIM } from './oidc.js';
|
|
6
|
+
export { safeNextPath } from './redirect.js';
|
package/dist/index.js
ADDED
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { type Auth, type AuthRequest, type GateRules, type Intent, type NewUser, type ResetError, type SignInError, type SignUpError, type User } from './auth.js';
|
|
3
|
+
import type { AuthOptions } from './config.js';
|
|
4
|
+
type SearchParams = Record<string, string | string[] | undefined>;
|
|
5
|
+
export interface NextAuth {
|
|
6
|
+
auth: Auth;
|
|
7
|
+
/** `export const { GET, POST } = handlers` from app/auth/[...auth]/route.ts. */
|
|
8
|
+
handlers: {
|
|
9
|
+
GET: (request: Request) => Promise<Response>;
|
|
10
|
+
POST: (request: Request) => Promise<Response>;
|
|
11
|
+
};
|
|
12
|
+
/** The gate and the refresh, for src/proxy.ts. */
|
|
13
|
+
proxy: (request: NextRequest, rules?: GateRules) => Promise<NextResponse>;
|
|
14
|
+
/** Reads the cookie, never writes it. Fresh only where the proxy ran. */
|
|
15
|
+
getUser: () => Promise<User | null>;
|
|
16
|
+
/** The user, or a redirect to sign in with `next` to come back to. */
|
|
17
|
+
requireUser: (next?: string) => Promise<User>;
|
|
18
|
+
/**
|
|
19
|
+
* For the app's login and register pages: the auth request the issuer sent
|
|
20
|
+
* the browser with, or a redirect that starts one. Pass the page's searchParams.
|
|
21
|
+
*/
|
|
22
|
+
authRequest: (searchParams: SearchParams | Promise<SearchParams>, opts?: {
|
|
23
|
+
next?: string;
|
|
24
|
+
intent?: Intent;
|
|
25
|
+
}) => Promise<AuthRequest>;
|
|
26
|
+
/** For a server action: redirects into the app on success, returns the error otherwise. */
|
|
27
|
+
signIn: (input: {
|
|
28
|
+
authRequestId: string;
|
|
29
|
+
email: string;
|
|
30
|
+
password: string;
|
|
31
|
+
}) => Promise<{
|
|
32
|
+
error: SignInError;
|
|
33
|
+
}>;
|
|
34
|
+
signUp: (input: {
|
|
35
|
+
authRequestId: string;
|
|
36
|
+
} & NewUser) => Promise<{
|
|
37
|
+
error: SignUpError;
|
|
38
|
+
message: string;
|
|
39
|
+
}>;
|
|
40
|
+
/** Emails a fresh verification link. Never says whether the address exists. */
|
|
41
|
+
resendVerification: (email: string, next?: string) => Promise<void>;
|
|
42
|
+
/** Emails a password-reset link. Never says whether the address exists. */
|
|
43
|
+
requestPasswordReset: (email: string, next?: string) => Promise<void>;
|
|
44
|
+
/** Emails a sign-in link. Never says whether the address exists. */
|
|
45
|
+
sendLink: (email: string, next?: string) => Promise<void>;
|
|
46
|
+
/** Brings a person into this app's organisation and mails them a way in. */
|
|
47
|
+
invite: (input: {
|
|
48
|
+
email: string;
|
|
49
|
+
givenName: string;
|
|
50
|
+
familyName: string;
|
|
51
|
+
next?: string;
|
|
52
|
+
}) => Promise<{
|
|
53
|
+
userId: string;
|
|
54
|
+
invited: boolean;
|
|
55
|
+
}>;
|
|
56
|
+
/** Sets the password from a reset link, signs the person in, and redirects. */
|
|
57
|
+
resetPassword: (input: {
|
|
58
|
+
userId: string;
|
|
59
|
+
code: string;
|
|
60
|
+
password: string;
|
|
61
|
+
next?: string;
|
|
62
|
+
}) => Promise<{
|
|
63
|
+
error: ResetError;
|
|
64
|
+
message: string;
|
|
65
|
+
}>;
|
|
66
|
+
}
|
|
67
|
+
export declare function nextAuth(options: AuthOptions): NextAuth;
|
|
68
|
+
export type { Auth, AuthOptions, AuthRequest, GateRules, Intent, NewUser, User };
|
package/dist/next.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { cookies } from 'next/headers';
|
|
2
|
+
import { redirect } from 'next/navigation';
|
|
3
|
+
import { NextResponse } from 'next/server';
|
|
4
|
+
import { createAuth, } from './auth.js';
|
|
5
|
+
import { withCookies } from './cookies.js';
|
|
6
|
+
export function nextAuth(options) {
|
|
7
|
+
const auth = createAuth(options);
|
|
8
|
+
const proxy = async (request, rules) => {
|
|
9
|
+
const gate = await auth.gate(request, rules);
|
|
10
|
+
let response;
|
|
11
|
+
if (gate.kind === 'redirect') {
|
|
12
|
+
response = NextResponse.redirect(gate.location, 303);
|
|
13
|
+
}
|
|
14
|
+
else if (gate.kind === 'deny') {
|
|
15
|
+
response = new NextResponse('Unauthorized', { status: 401 });
|
|
16
|
+
}
|
|
17
|
+
else if (gate.cookies.length > 0) {
|
|
18
|
+
const headers = new Headers(request.headers);
|
|
19
|
+
headers.set('cookie', withCookies(request.headers.get('cookie'), gate.cookies));
|
|
20
|
+
response = NextResponse.next({ request: { headers } });
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
response = NextResponse.next();
|
|
24
|
+
}
|
|
25
|
+
for (const cookie of gate.cookies)
|
|
26
|
+
response.headers.append('Set-Cookie', cookie.header);
|
|
27
|
+
return response;
|
|
28
|
+
};
|
|
29
|
+
const getUser = async () => {
|
|
30
|
+
const store = await cookies();
|
|
31
|
+
const { user } = await auth.readCookie(store.get(auth.sessionCookieName)?.value, {
|
|
32
|
+
refresh: false,
|
|
33
|
+
});
|
|
34
|
+
return user;
|
|
35
|
+
};
|
|
36
|
+
const requireUser = async (next) => {
|
|
37
|
+
const user = await getUser();
|
|
38
|
+
if (!user)
|
|
39
|
+
redirect(auth.startUrl(next));
|
|
40
|
+
return user;
|
|
41
|
+
};
|
|
42
|
+
const authRequest = async (searchParams, opts = {}) => {
|
|
43
|
+
const params = await searchParams;
|
|
44
|
+
const id = typeof params.authRequest === 'string' ? params.authRequest : null;
|
|
45
|
+
if (id) {
|
|
46
|
+
try {
|
|
47
|
+
return await auth.authRequest(id);
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
}
|
|
51
|
+
redirect(auth.startUrl(opts.next, opts.intent));
|
|
52
|
+
};
|
|
53
|
+
// A server action cannot redirect into a route handler, so the callback runs here and the cookies are set on the action's response.
|
|
54
|
+
const finish = async (callbackUrl) => {
|
|
55
|
+
const store = await cookies();
|
|
56
|
+
const header = store
|
|
57
|
+
.getAll()
|
|
58
|
+
.map((c) => `${c.name}=${c.value}`)
|
|
59
|
+
.join('; ');
|
|
60
|
+
const { location, cookies: set } = await auth.complete(callbackUrl, header);
|
|
61
|
+
for (const c of set) {
|
|
62
|
+
if (c.value)
|
|
63
|
+
store.set(c.name, c.value, c.attributes);
|
|
64
|
+
else
|
|
65
|
+
store.delete({ name: c.name, path: '/' });
|
|
66
|
+
}
|
|
67
|
+
const url = new URL(location);
|
|
68
|
+
redirect(`${url.pathname}${url.search}`);
|
|
69
|
+
};
|
|
70
|
+
const resetPassword = async (input) => {
|
|
71
|
+
const result = await auth.resetPassword(input);
|
|
72
|
+
if (!result.ok)
|
|
73
|
+
return { error: result.error, message: result.message };
|
|
74
|
+
const store = await cookies();
|
|
75
|
+
for (const c of result.cookies) {
|
|
76
|
+
if (c.value)
|
|
77
|
+
store.set(c.name, c.value, c.attributes);
|
|
78
|
+
else
|
|
79
|
+
store.delete({ name: c.name, path: '/' });
|
|
80
|
+
}
|
|
81
|
+
const url = new URL(result.location);
|
|
82
|
+
redirect(`${url.pathname}${url.search}`);
|
|
83
|
+
};
|
|
84
|
+
const signIn = async (input) => {
|
|
85
|
+
const result = await auth.signIn(input);
|
|
86
|
+
if (result.ok)
|
|
87
|
+
return finish(result.redirectTo);
|
|
88
|
+
return { error: result.error };
|
|
89
|
+
};
|
|
90
|
+
const signUp = async (input) => {
|
|
91
|
+
const result = await auth.signUp(input);
|
|
92
|
+
if (result.ok)
|
|
93
|
+
return finish(result.redirectTo);
|
|
94
|
+
return { error: result.error, message: result.message };
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
auth,
|
|
98
|
+
handlers: { GET: auth.handle, POST: auth.handle },
|
|
99
|
+
proxy,
|
|
100
|
+
getUser,
|
|
101
|
+
requireUser,
|
|
102
|
+
authRequest,
|
|
103
|
+
signIn,
|
|
104
|
+
signUp,
|
|
105
|
+
resendVerification: auth.resendVerification,
|
|
106
|
+
requestPasswordReset: auth.requestPasswordReset,
|
|
107
|
+
sendLink: auth.sendLink,
|
|
108
|
+
invite: auth.invite,
|
|
109
|
+
resetPassword,
|
|
110
|
+
};
|
|
111
|
+
}
|
package/dist/oidc.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as jose from 'jose';
|
|
2
|
+
import type { ResolvedOptions } from './config.js';
|
|
3
|
+
export declare const ORG_CLAIM = "urn:zitadel:iam:user:resourceowner:id";
|
|
4
|
+
export interface Tokens {
|
|
5
|
+
idToken: string;
|
|
6
|
+
refreshToken?: string;
|
|
7
|
+
claims: jose.JWTPayload;
|
|
8
|
+
}
|
|
9
|
+
export declare class Oidc {
|
|
10
|
+
private readonly options;
|
|
11
|
+
private configuration;
|
|
12
|
+
private jwks;
|
|
13
|
+
private refreshing;
|
|
14
|
+
constructor(options: ResolvedOptions);
|
|
15
|
+
private config;
|
|
16
|
+
private keys;
|
|
17
|
+
get redirectUri(): URL;
|
|
18
|
+
authorizationUrl(state: string, nonce: string, codeVerifier: string, { create }?: {
|
|
19
|
+
create?: boolean;
|
|
20
|
+
}): Promise<URL>;
|
|
21
|
+
exchange(callbackParams: URLSearchParams, transaction: {
|
|
22
|
+
state: string;
|
|
23
|
+
nonce: string;
|
|
24
|
+
codeVerifier: string;
|
|
25
|
+
}): Promise<Tokens>;
|
|
26
|
+
refresh(refreshToken: string): Promise<Tokens>;
|
|
27
|
+
private tokensFrom;
|
|
28
|
+
verify(idToken: string): Promise<jose.JWTPayload>;
|
|
29
|
+
private assertOrganization;
|
|
30
|
+
}
|
|
31
|
+
export type AuthErrorReason = 'expired' | 'state' | 'denied' | 'exchange' | 'organization' | 'cookie' | 'request';
|
|
32
|
+
export declare class AuthError extends Error {
|
|
33
|
+
readonly reason: AuthErrorReason;
|
|
34
|
+
constructor(reason: AuthErrorReason, message: string);
|
|
35
|
+
}
|