From b0c777dc62b67bc987ad0054d52f6b28047afeed Mon Sep 17 00:00:00 2001 From: Danilo Alonso Date: Tue, 19 Mar 2024 09:35:45 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/index.d.ts | 305 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 4 +- test/types/index.ts | 143 +++++++++++++++++++++ 3 files changed, 451 insertions(+), 1 deletion(-) create mode 100644 lib/index.d.ts create mode 100644 test/types/index.ts diff --git a/lib/index.d.ts b/lib/index.d.ts new file mode 100644 index 0000000..065ca2a --- /dev/null +++ b/lib/index.d.ts @@ -0,0 +1,305 @@ +import { AuthCredentials, Plugin, Request } from '@hapi/hapi'; + +declare module '@hapi/hapi' { + interface ServerAuth { + strategy(name: string, scheme: 'bell', options: BellOptions): void; + } +} + +export interface StringLikeMap { + [key: string]: string | number; +} +export type Provider = + | 'arcgisonline' + | 'auth0' + | 'azure' + | 'bitbucket' + | 'cognito' + | 'digitalocean' + | 'discord' + | 'dropbox' + | 'facebook' + | 'fitbit' + | 'foursquare' + | 'github' + | 'gitlab' + | 'google' + | 'googleplus' + | 'instagram' + | 'linkedin' + | 'live' + | 'medium' + | 'meetup' + | 'mixer' + | 'nest' + | 'okta' + | 'phabricator' + | 'pingfed' + | 'pinterest' + | 'reddit' + | 'salesforce' + | 'slack' + | 'spotify' + | 'stripe' + | 'trakt' + | 'tumblr' + | 'twitch' + | 'twitter' + | 'vk' + | 'wordpress' + | 'yahoo'; + +export type RequestPassThrough = (request: Request) => PromiseLike | AuthCredentials; + +export interface OptionalOptions { + /** + * the name of the cookie used to manage the temporary state. + * Defaults to 'bell-provider' where 'provider' is the provider name (or 'custom' for custom providers). + * For example, the Twitter cookie name defaults to 'bell-twitter'. + */ + cookie?: string | undefined; + /** + * sets the cookie secure flag. + * Defaults to true. + */ + isSecure?: boolean | undefined; + /** + * sets the cookie HTTP only flag. + * Defaults to true. + */ + isHttpOnly?: boolean | undefined; + /** + * cookie time-to-live in milliseconds. + * Defaults to null (session time-life - cookies are deleted when the browser is closed). + */ + ttl?: number | undefined; + /** + * the domain scope. + * Defaults to null (no domain). + */ + domain?: string | undefined; + /** + * provider-specific query parameters for the authentication endpoint. + * It may be passed either as an object to merge into the query string, + * or a function which takes the client's request and returns an object. + * Each provider supports its own set of parameters which customize the user's login experience. + * For example: + * * Facebook supports `display` ('page', 'popup', or 'touch'), `auth_type`, `auth_nonce`. + * * Google supports `access_type`, `approval_prompt`, `prompt`, `login_hint`, `user_id`, `hd`. + * * Twitter supports `force_login`, `screen_name`. + * * Linkedin supports `fields`. + */ + providerParams?: StringLikeMap | ((request: Request) => StringLikeMap) | undefined; + /** + * allows passing query parameters from a bell protected endpoint to the auth request. + * It will merge the query params you pass along with the providerParams and any other predefined ones. + * Be aware that this will override predefined query parameters! + * Default to false. + */ + allowRuntimeProviderParams?: StringLikeMap | boolean | undefined; + /** + * Each built-in vendor comes with the required scope for basic profile information. + * Use scope to specify a different scope as required by your application. + * It may be passed either as an object to merge into the query string, + * or a function which takes the client's request and returns an object. + * Consult the provider for their specific supported scopes. + */ + scope?: string[] | ((request: Request) => string[]) | undefined; + /** + * skips obtaining a user profile from the provider. + * Useful if you need specific scopes, + * but not the user profile. + * Defaults to false. + */ + skipProfile?: boolean | undefined; + /** + * a configuration object used to customize the provider settings. + * The built-in 'twitter' provider accepts the `extendedProfile` & `getMethod` options. + * option which allows disabling the extra profile request when the provider + * returns the user information in the callback (defaults to true). + * The built-in 'github' and 'phabricator' providers accept the uri + * option which allows pointing to a private enterprise installation (e.g. 'https://vpn.example.com'). + * See Providers documentation for more information. + */ + config?: + | { extendedProfile?: boolean | undefined; getMethod?: string | undefined } + | { uri?: string | undefined } + | undefined; + /** + * an object of key-value pairs that specify additional + * URL query parameters to send with the profile request to the provider. + * The built-in facebook provider, + * for example, could have fields specified to determine the fields returned from the user's graph, + * which would then be available to you in the auth.credentials.profile.raw object. + */ + profileParams?: StringLikeMap | undefined; + /** + * allows passing additional OAuth state from initial request. + * This must be a function returning a string, + * which will be appended to the bell internal state parameter for OAuth code flow. + */ + runtimeStateCallback?(req: Request): string; + + // THESE ARE IN THE *REQUIRED* section but are actually not... + /** + * A boolean indicating whether or not you want the redirect_uri to be forced to https. + * Useful if your hapi application runs as http, but is accessed through https. + */ + forceHttps?: boolean | undefined; + /** + * Set the base redirect_uri manually if it cannot be inferred properly from server settings. + * Useful to override port, protocol, and host if proxied or forwarded. + */ + location?: string | ((req: Request) => string) | undefined; +} + +export interface RequiredProviderOptions { + /** + * the cookie encryption password. + * Used to encrypt the temporary state cookie used by the module in + * between the authorization protocol steps. + */ + password: string; + /** + * the OAuth client identifier (consumer key). + */ + clientId: string; + /** + * the OAuth client secret (consumer secret) + */ + clientSecret: string; +} + +export interface KnownProviderOptions extends RequiredProviderOptions, OptionalOptions { + provider: Provider; +} + +/** + * @param uri the requested resource URI (bell will add the token or authentication header as needed). + * @param params any URI query parameters (cannot include them in the URI due to signature requirements). + */ +export type AuthedRequest = (uri: string, params?: { [key: string]: string }) => Promise; + +export interface Credentials { + provider: Provider | 'custom'; + token: string; + query: StringLikeMap; + /** + * Varying data depending on provider. + */ + profile?: object | undefined; +} + +export interface Credentials1 extends Credentials { + secret: string; +} + +export interface Credentials2 extends Credentials { + refreshToken?: string | undefined; + expiresIn?: number | undefined; +} + +export interface CustomProtocol { + /** + * The name of the protocol. + * @default custom + */ + name?: string | undefined; + /** + * the authorization endpoint URI. + */ + auth: string; + /** + * the access token endpoint URI. + */ + token: string; + /** + * a headers object with additional headers required by the provider + * (e.g. GitHub required the 'User-Agent' header which is set by default). + */ + headers?: { + [key: string]: string; + } | undefined; +} + +/** + * a function used to obtain user profile information and normalize it. + * @param credentials the credentials object. + * Change the object directly within the function (profile information is typically stored under credentials.profile). + * @param params the parsed information received from the provider (e.g. token, secret, and other custom fields). + * @param get an OAuth helper function to make authenticated requests using the credentials received. + */ +export type ProfileGetter = ( + this: CustomProviderOptions, + credentials: C, + params: { [key: string]: string }, + get: AuthedRequest, +) => Promise; + +export interface CustomProtocol1 extends CustomProtocol { + /** + * the authorization protocol used. + */ + protocol: 'oauth'; + + /** + * the OAuth signature method. Must be one of: + * * 'HMAC-SHA1' - default + * * 'RSA-SHA1' - in that case, the clientSecret is your RSA private key + */ + signatureMethod?: 'HMAC-SHA1' | 'RSA-SHA1' | undefined; + /** + * the temporary credentials (request token) endpoint). + */ + temporary?: string | undefined; + + profile: ProfileGetter; +} + +export type PkceSetting = 'plain' | 'S256'; + +export interface CustomProtocol2 extends CustomProtocol { + /** + * the authorization protocol used. + */ + protocol: 'oauth2'; + /** + * an array of scope strings. + */ + scope?: string[] | ((query: StringLikeMap) => string[]) | undefined; + /** + * boolean that determines if OAuth client id and client secret will be sent + * as parameters as opposed to an Authorization header. + * Defaults to false. + */ + useParamsAuth?: boolean | undefined; + + /** + * If specified, uses proof key exchange. + */ + pkce?: PkceSetting | undefined; + + /** + * the scope separator character. Only required when a provider has a broken OAuth 2.0 implementation. Defaults to space (Facebook and GitHub default to comma). + */ + scopeSeparator?: string | undefined; + + profile: ProfileGetter; +} + +export interface CustomProviderOptions extends RequiredProviderOptions, OptionalOptions { + provider: CustomProtocol1 | CustomProtocol2; +} + +export type BellOptions = CustomProviderOptions | KnownProviderOptions; + +export const plugin: Plugin; +/** + * Enables simulation mode. + */ +export function simulate(credentialsFunc: RequestPassThrough): void; +/** + * [See docs](https://github.com/hapijs/bell/blob/master/API.md#simulated-authentication) + * Disables simulation mode + */ +export function simulate(state: false): void; \ No newline at end of file diff --git a/package.json b/package.json index 85655a1..e88e00b 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "version": "13.0.1", "repository": "git://github.com/hapijs/bell", "main": "lib/index.js", + "types": "lib/index.d.ts", "engines": { "node": ">=14.0.0" }, @@ -62,7 +63,8 @@ "@hapi/hapi": "^21.2.1", "@hapi/hawk": "^8.0.0", "@hapi/lab": "^25.0.1", - "@hapi/teamwork": "^6.0.0" + "@hapi/teamwork": "^6.0.0", + "typescript": "^5.4.2" }, "scripts": { "test": "lab -a @hapi/code -t 100 -L -m 3000", diff --git a/test/types/index.ts b/test/types/index.ts new file mode 100644 index 0000000..de7e765 --- /dev/null +++ b/test/types/index.ts @@ -0,0 +1,143 @@ +import lab from '@hapi/lab'; + +import * as Bell from '../..'; +import { Server } from '@hapi/hapi'; + +async function run() { + const server = new Server({ port: 8000 }); + await server.register(Bell); + + + + Bell.simulate(async () => ({})); + Bell.simulate(() => ({})); + + server.auth.strategy('arcgisonline', 'bell', { + provider: 'twitter', + password: 'some cookie password', + location: 'http://example.com/oauth', + clientId: '', + clientSecret: '', + scope(request) { + const scopes = ['public_profile', 'email']; + if (request.query.wantsSharePermission) { + scopes.push('publish_actions'); + } + return scopes; + }, + }); + + + const providers: Bell.BellOptions[] = [{ + provider: 'discord', + password: 'cookie_encryption_password_secure', + isSecure: false, + clientId: '', + clientSecret: '', + location: () => '', + }, { + provider: 'facebook', + password: 'cookie_encryption_password_secure', + isSecure: false, + clientId: '', + clientSecret: '', + location: server.info.uri, + }, { + provider: 'google', + password: 'cookie_encryption_password_secure', + isSecure: false, + clientId: '', + clientSecret: '', + location: server.info.uri, + }, { + provider: 'linkedin', + password: 'cookie_encryption_password_secure', + isSecure: false, + clientId: '', + clientSecret: '', + providerParams: { + redirect_uri: server.info.uri + '/bell/door', + }, + }, { + provider: 'meetup', + password: 'cookie_encryption_password_secure', + isSecure: false, + clientId: '', + clientSecret: '', + scope: ['basic', 'ageless', 'group_edit', 'reporting'], + }, { + provider: 'nest', + password: 'cookie_encryption_password_secure', + isSecure: false, + clientId: '', + clientSecret: '', + }, { + provider: 'okta', + config: { uri: 'https://your-organization.okta.com' }, + password: 'cookie_encryption_password_secure', + isSecure: false, + location: 'http://127.0.0.1:8000', + clientId: '', + clientSecret: '', + }, { + provider: 'slack', + password: 'cookie_encryption_password_secure', + isSecure: false, + clientId: '', + clientSecret: '', + }, { + provider: 'twitch', + password: 'cookie_encryption_password_secure', + isSecure: false, + clientId: '', + clientSecret: '', + scope: ['user_read', 'channel_read'], + }, { + provider: { + auth: 'http://test.com/auth', + token: 'http://test.com/auth', + name: 'custom', + protocol: 'oauth', + temporary: 'wat', + async profile(credentials, params, get) { + console.log(credentials.provider); + console.log(credentials.query); + console.log(credentials.secret); + console.log(this.clientId); + credentials.profile = await get('http://test.com/profile', { + a: 'test', + }); + }, + }, + password: 'cookie_encryption_password_secure', + isSecure: false, + clientId: '', + clientSecret: '', + location: () => '', + }, { + provider: { + auth: 'http://test.com/auth', + token: 'http://test.com/auth', + name: 'custom', + protocol: 'oauth2', + scope: ['a', 's', 'd', 'f'], + scopeSeparator: '~~~', + pkce: 'S256', + async profile(credentials, params, get) { + console.log(credentials.provider); + console.log(credentials.query); + console.log(credentials.token); + console.log(credentials.refreshToken); + console.log(this.clientId); + credentials.profile = await get('http://test.com/profile', { + a: 'test', + }); + }, + }, + password: 'cookie_encryption_password_secure', + isSecure: false, + clientId: '', + clientSecret: '', + location: () => '', + }]; +} \ No newline at end of file From 4ff1b9bbb93b4123b5548e8bc0d283e753ed2053 Mon Sep 17 00:00:00 2001 From: Danilo Alonso Date: Tue, 19 Mar 2024 09:50:38 -0400 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=F0=9F=90=9B=20Run=20lab=20TS=20test?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 ++- test/types/index.ts | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index e88e00b..22a5f51 100644 --- a/package.json +++ b/package.json @@ -64,10 +64,11 @@ "@hapi/hawk": "^8.0.0", "@hapi/lab": "^25.0.1", "@hapi/teamwork": "^6.0.0", + "@types/node": "^20.11.30", "typescript": "^5.4.2" }, "scripts": { - "test": "lab -a @hapi/code -t 100 -L -m 3000", + "test": "lab -a @hapi/code -t 100 -L -m 3000 -Y", "test-cov-html": "lab -a @hapi/code -r html -o coverage.html -m 3000" }, "license": "BSD-3-Clause" diff --git a/test/types/index.ts b/test/types/index.ts index de7e765..f2eb0c5 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -1,5 +1,3 @@ -import lab from '@hapi/lab'; - import * as Bell from '../..'; import { Server } from '@hapi/hapi'; @@ -7,8 +5,6 @@ async function run() { const server = new Server({ port: 8000 }); await server.register(Bell); - - Bell.simulate(async () => ({})); Bell.simulate(() => ({}));