@theshelf/authentication 0.0.1

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 ADDED
@@ -0,0 +1,115 @@
1
+
2
+ # Authentication | The Shelf
3
+
4
+ The authentication package provides a universal interaction layer with an actual identity provider solution.
5
+
6
+ This package is based on the following authentication flow:
7
+
8
+ 1. Browser redirects to the IDP login page.
9
+ 2. User authenticate at the IDP.
10
+ 3. IDP provides identity information to this package.
11
+ 4. This package starts a session based on the provided identity.
12
+ 5. Sessions can be refreshed via this package.
13
+ 6. Until the user logs out via this package.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @theshelf/authentication
19
+ ```
20
+
21
+ ## Implementations
22
+
23
+ Currently, there is only one implementation:
24
+
25
+ * **OpenID** - persistent document storage.
26
+
27
+ ## Configuration
28
+
29
+ The used implementation needs to be configured in the `.env` file, together with the client URL.
30
+
31
+ ```env
32
+ AUTHENTICATION_IMPLEMENTATION="openid"
33
+ AUTHENTICATION_CLIENT_URI="https://application.com/authenticate"
34
+ ```
35
+
36
+ In case of OpenID, additional configuration is required.
37
+
38
+ ```env
39
+ OPENID_ISSUER="https://identityprovider.com"
40
+ OPENID_CLIENT_ID="openid"
41
+ OPENID_CLIENT_SECRET=""
42
+ OPENID_REDIRECT_PATH="https://application.com/login"
43
+ OPENID_ALLOW_INSECURE_REQUESTS=false
44
+ ```
45
+
46
+ ## How to use
47
+
48
+ An instance of the configured identity provider implementation can be imported for performing authentication operations.
49
+
50
+ ```ts
51
+ import identityProvider from '@theshelf/authentication';
52
+
53
+ // Perform operations with the identityProvider instance
54
+ ```
55
+
56
+ ### Operations
57
+
58
+ ```ts
59
+ import identityProvider, { Session } from '@theshelf/authentication';
60
+
61
+ // Open connection
62
+ await identityProvider.connect();
63
+
64
+ // Close connection
65
+ await identityProvider.disconnect();
66
+
67
+ // Request the URL of the login page
68
+ const loginUrl: string = await identityProvider.getLoginUrl();
69
+
70
+ // Handle a login and get a session
71
+ // Throws `LoginFailed` if not successful
72
+ const firstSession: Session = await identityProvider.login(providedIdentity);
73
+
74
+ // Refresh a session
75
+ // Throws `LoginFailed` if not successful
76
+ const secondSession: Session = await identityProvider.refresh(firstSession);
77
+
78
+ // Logout
79
+ await identityProvider.logout(secondSession);
80
+ ```
81
+
82
+ ### Session structure
83
+
84
+ The session has the following structure.
85
+
86
+ ```ts
87
+ type Session = {
88
+ key?: string;
89
+ requester?: unknown;
90
+ identity: Identity;
91
+ accessToken: Token;
92
+ refreshToken: Token;
93
+ expires: Date;
94
+ };
95
+ ```
96
+
97
+ Every session has a unique key that will be provided to external clients. This key is an unrelated hash value that contains no session information. It's ony used for referencing and storage.
98
+
99
+ The requester represents the actual logged in user retrieved from the identity information (email), and can be stored in the session for quick reference. The full Identity structure looks like this.
100
+
101
+ ```ts
102
+ type Identity = {
103
+ name: string;
104
+ nickname: string | undefined;
105
+ picture: string | undefined;
106
+ email: string;
107
+ email_verified: boolean;
108
+ };
109
+ ```
110
+
111
+ The access and refresh tokens can be of any type, but is always represented as string. This depends on the configured implementation. In most cases this will be a JWT.
112
+
113
+ ```ts
114
+ type Token = string;
115
+ ```
@@ -0,0 +1,10 @@
1
+ import type { Session } from './types';
2
+ export interface IdentityProvider {
3
+ get connected(): boolean;
4
+ connect(): Promise<void>;
5
+ disconnect(): Promise<void>;
6
+ getLoginUrl(origin: string): Promise<string>;
7
+ login(origin: string, data: Record<string, unknown>): Promise<Session>;
8
+ refresh(session: Session): Promise<Session>;
9
+ logout(session: Session): Promise<void>;
10
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ type Identity = {
2
+ name: string;
3
+ nickname: string | undefined;
4
+ picture: string | undefined;
5
+ email: string;
6
+ email_verified: boolean;
7
+ };
8
+ type Token = string;
9
+ type Session = {
10
+ key?: string;
11
+ requester?: unknown;
12
+ identity: Identity;
13
+ accessToken: Token;
14
+ refreshToken: Token;
15
+ expires: Date;
16
+ };
17
+ export type { Identity, Session };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export default class AuthenticationError extends Error {
2
+ }
@@ -0,0 +1,2 @@
1
+ export default class AuthenticationError extends Error {
2
+ }
@@ -0,0 +1,3 @@
1
+ import AuthenticationError from './AuthenticationError';
2
+ export default class LoginFailed extends AuthenticationError {
3
+ }
@@ -0,0 +1,3 @@
1
+ import AuthenticationError from './AuthenticationError';
2
+ export default class LoginFailed extends AuthenticationError {
3
+ }
@@ -0,0 +1,4 @@
1
+ import AuthenticationError from './AuthenticationError';
2
+ export default class NotConnected extends AuthenticationError {
3
+ constructor(message?: string);
4
+ }
@@ -0,0 +1,6 @@
1
+ import AuthenticationError from './AuthenticationError';
2
+ export default class NotConnected extends AuthenticationError {
3
+ constructor(message) {
4
+ super(message ?? 'Identity provider not connected');
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ import AuthenticationError from './AuthenticationError';
2
+ export default class UnknownImplementation extends AuthenticationError {
3
+ constructor(name: string);
4
+ }
@@ -0,0 +1,6 @@
1
+ import AuthenticationError from './AuthenticationError';
2
+ export default class UnknownImplementation extends AuthenticationError {
3
+ constructor(name) {
4
+ super(`Unknown authentication implementation: ${name}`);
5
+ }
6
+ }
@@ -0,0 +1,3 @@
1
+ import type { IdentityProvider } from './definitions/interfaces';
2
+ declare const _default: IdentityProvider;
3
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import UnknownImplementation from './errors/UnknownImplementation';
2
+ import createOpenID from './implementations/openid/create';
3
+ const implementations = new Map([
4
+ ['openid', createOpenID],
5
+ ]);
6
+ const DEFAULT_AUTHENTICATION_IMPLEMENTATION = 'openid';
7
+ const implementationName = process.env.AUTHENTICATION_IMPLEMENTATION ?? DEFAULT_AUTHENTICATION_IMPLEMENTATION;
8
+ const creator = implementations.get(implementationName.toLowerCase());
9
+ if (creator === undefined) {
10
+ throw new UnknownImplementation(implementationName);
11
+ }
12
+ export default creator();
@@ -0,0 +1,21 @@
1
+ import type { IdentityProvider } from '../../definitions/interfaces';
2
+ import type { Session } from '../../definitions/types';
3
+ type OpenIDConfiguration = {
4
+ issuer: string;
5
+ clientId: string;
6
+ clientSecret: string;
7
+ redirectPath: string;
8
+ allowInsecureRequests: boolean;
9
+ };
10
+ export default class OpenID implements IdentityProvider {
11
+ #private;
12
+ constructor(configuration: OpenIDConfiguration);
13
+ get connected(): boolean;
14
+ connect(): Promise<void>;
15
+ disconnect(): Promise<void>;
16
+ getLoginUrl(origin: string): Promise<string>;
17
+ login(origin: string, data: Record<string, unknown>): Promise<Session>;
18
+ refresh(session: Session): Promise<Session>;
19
+ logout(session: Session): Promise<void>;
20
+ }
21
+ export {};
@@ -0,0 +1,105 @@
1
+ import { allowInsecureRequests, authorizationCodeGrant, buildAuthorizationUrlWithPAR, calculatePKCECodeChallenge, discovery, fetchUserInfo, randomPKCECodeVerifier, refreshTokenGrant, tokenRevocation } from 'openid-client';
2
+ import LoginFailed from '../../errors/LoginFailed';
3
+ import NotConnected from '../../errors/NotConnected';
4
+ export default class OpenID {
5
+ #providerConfiguration;
6
+ #clientConfiguration;
7
+ #codeVerifier = randomPKCECodeVerifier();
8
+ constructor(configuration) {
9
+ this.#providerConfiguration = configuration;
10
+ }
11
+ get connected() {
12
+ return this.#clientConfiguration !== undefined;
13
+ }
14
+ async connect() {
15
+ const issuer = new URL(this.#providerConfiguration.issuer);
16
+ const clientId = this.#providerConfiguration.clientId;
17
+ const clientSecret = this.#providerConfiguration.clientSecret;
18
+ const requestOptions = this.#getRequestOptions();
19
+ this.#clientConfiguration = await discovery(issuer, clientId, clientSecret, undefined, requestOptions);
20
+ }
21
+ async disconnect() {
22
+ this.#clientConfiguration = undefined;
23
+ }
24
+ async getLoginUrl(origin) {
25
+ const redirect_uri = new URL(this.#providerConfiguration.redirectPath, origin).href;
26
+ const scope = 'openid profile email';
27
+ const code_challenge = await calculatePKCECodeChallenge(this.#codeVerifier);
28
+ const code_challenge_method = 'S256';
29
+ const parameters = {
30
+ redirect_uri,
31
+ scope,
32
+ code_challenge,
33
+ code_challenge_method
34
+ };
35
+ const clientConfiguration = this.#getClientConfiguration();
36
+ const redirectTo = await buildAuthorizationUrlWithPAR(clientConfiguration, parameters);
37
+ return redirectTo.href;
38
+ }
39
+ async login(origin, data) {
40
+ const clientConfiguration = this.#getClientConfiguration();
41
+ const url = new URL(this.#providerConfiguration.redirectPath, origin);
42
+ url.searchParams.set('session_state', data.session_state);
43
+ url.searchParams.set('iss', data.iss);
44
+ url.searchParams.set('code', data.code);
45
+ const tokens = await authorizationCodeGrant(clientConfiguration, url, {
46
+ pkceCodeVerifier: this.#codeVerifier,
47
+ idTokenExpected: true
48
+ });
49
+ const access_token = tokens.access_token;
50
+ const claims = this.#getClaims(tokens);
51
+ const sub = claims.sub;
52
+ const expires = claims.exp * 1000;
53
+ const userInfo = await fetchUserInfo(clientConfiguration, access_token, sub);
54
+ const identity = {
55
+ name: userInfo.name,
56
+ nickname: userInfo.nickname,
57
+ picture: userInfo.picture,
58
+ email: userInfo.email,
59
+ email_verified: userInfo.email_verified
60
+ };
61
+ return {
62
+ identity: identity,
63
+ accessToken: tokens.access_token,
64
+ refreshToken: tokens.refresh_token,
65
+ expires: new Date(expires)
66
+ };
67
+ }
68
+ async refresh(session) {
69
+ const config = this.#getClientConfiguration();
70
+ const tokens = await refreshTokenGrant(config, session.refreshToken);
71
+ const claims = this.#getClaims(tokens);
72
+ const expires = claims.exp * 1000;
73
+ return {
74
+ requester: session.requester,
75
+ identity: session.identity,
76
+ accessToken: tokens.access_token,
77
+ refreshToken: tokens.refresh_token,
78
+ expires: new Date(expires)
79
+ };
80
+ }
81
+ logout(session) {
82
+ const config = this.#getClientConfiguration();
83
+ return tokenRevocation(config, session.refreshToken);
84
+ }
85
+ #getClientConfiguration() {
86
+ if (this.#clientConfiguration === undefined) {
87
+ throw new NotConnected('OpenID client not connected');
88
+ }
89
+ return this.#clientConfiguration;
90
+ }
91
+ #getRequestOptions() {
92
+ const options = {};
93
+ if (this.#providerConfiguration.allowInsecureRequests) {
94
+ options.execute = [allowInsecureRequests];
95
+ }
96
+ return options;
97
+ }
98
+ #getClaims(tokens) {
99
+ const claims = tokens.claims();
100
+ if (claims === undefined) {
101
+ throw new LoginFailed('No claims in ID token');
102
+ }
103
+ return claims;
104
+ }
105
+ }
@@ -0,0 +1,2 @@
1
+ import OpenID from './OpenID';
2
+ export default function create(): OpenID;
@@ -0,0 +1,9 @@
1
+ import OpenID from './OpenID';
2
+ export default function create() {
3
+ const issuer = process.env.OPENID_ISSUER ?? 'undefined';
4
+ const clientId = process.env.OPENID_CLIENT_ID ?? 'undefined';
5
+ const clientSecret = process.env.OPENID_CLIENT_SECRET ?? 'undefined';
6
+ const redirectPath = process.env.OPENID_REDIRECT_PATH ?? 'undefined';
7
+ const allowInsecureRequests = process.env.OPENID_ALLOW_INSECURE_REQUESTS === 'true';
8
+ return new OpenID({ issuer, clientId, clientSecret, redirectPath, allowInsecureRequests });
9
+ }
@@ -0,0 +1,6 @@
1
+ export * from './definitions/interfaces';
2
+ export * from './definitions/types';
3
+ export { default as AuthenticationError } from './errors/AuthenticationError';
4
+ export { default as NotConnected } from './errors/NotConnected';
5
+ export { default as UnknownImplementation } from './errors/UnknownImplementation';
6
+ export { default } from './implementation';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from './definitions/interfaces';
2
+ export * from './definitions/types';
3
+ export { default as AuthenticationError } from './errors/AuthenticationError';
4
+ export { default as NotConnected } from './errors/NotConnected';
5
+ export { default as UnknownImplementation } from './errors/UnknownImplementation';
6
+ export { default } from './implementation';
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@theshelf/authentication",
3
+ "private": false,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "clean": "rimraf dist",
9
+ "lint": "eslint",
10
+ "review": "npm run build && npm run lint",
11
+ "prepublishOnly": "npm run clean && npm run build"
12
+ },
13
+ "files": [
14
+ "README.md",
15
+ "dist"
16
+ ],
17
+ "types": "dist/index.d.ts",
18
+ "exports": "./dist/index.js",
19
+ "dependencies": {
20
+ "openid-client": "6.8.1"
21
+ }
22
+ }