├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── Strategies
├── Auth.ts
├── MFA
│ ├── LocalAuth.ts
│ ├── totp.ts
│ └── twilio.ts
└── OAuth
│ ├── DiscordOAuth.ts
│ ├── FacebookOAuth.ts
│ ├── GithubOAuth.ts
│ ├── GoogleOAuth.ts
│ ├── LinkedinOAuth.ts
│ ├── OAuth.ts
│ └── TwitterOAuth.ts
├── __tests__
├── DiscordOAuth-test.ts
├── FacebookOAuth-test.ts
├── GithubOAuth-test.ts
├── GoogleOAuth-test.ts
├── LinkedinOAuth-test.ts
├── LocalAuth-test.ts
├── TwitterOAuth-test.ts
├── bedrock-test.ts
└── twilio-test.ts
├── deps.ts
├── mod.ts
├── strategies.ts
└── types.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .vscode
3 | .package-lock.json
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true,
3 | "deno.lint": false,
4 | "deno.unstable": false
5 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bedrock
2 |
3 |
4 |
5 |

6 |
7 | A fully modular authentication library for Deno that intends to be the *Bedrock* of your application's authentication/session control. Bedrock provides authentication (Local + MFA, OAuth), and session handling middleware as well as conditional access controls to restrict access to your routes as defined by your application's requirements.
8 |
9 | Check our website [here](https://bedrockauth.dev) for additional information and documentation!
10 |
11 |
12 | ### Importing Bedrock
13 | ```typescript
14 | import { init } from 'https://deno.land/x/bedrock/mod.ts'
15 | ```
16 | ## Implementation
17 |
18 | ### Choose your desired strategy or strategies.
19 | Bedrock offers several ways to provide multi-factor authentication through a local authentication strategy. These include TOTP through an authentication app, SMS, and Email. Additionally, we have abstracted away the process of implementing these six popular OAuth providers.
20 | - Discord
21 | - Facebook
22 | - Github
23 | - Google
24 | - LinkedIn
25 | - Twitter
26 | ## Local Authentication Strategy
27 |
28 | ### Initiate Bedrock
29 | Initiate a Bedrock class object passing in params. Implementing your choice of strategies will require some variance in your parameters object. Visit our documentation for more information about which parameters you will need for your desired implementation.
30 | ```typescript
31 | const Bedrock = init({
32 | checkCreds : dbController.checkCreds,
33 | mfaType: 'Token',
34 | getSecret: dbController.getSecret,
35 | };
36 | ```
37 |
38 | ### Implant Bedrock middleware into your routes
39 | ```typescript
40 | // Verification of username/password and creation of session
41 | MFARouter.post('/login', Bedrock.localLogin, (ctx: Context) => {
42 | if(ctx.state.localVerified) {
43 | //inside this if statement means user is locally authenticated with username/password
44 | if (ctx.state.hasSecret === false) {
45 | //inside this if statement means user is locally authenticated
46 | //but does not have a stored secret
47 | ctx.response.body = {
48 | successful: true;
49 | mfaRequired: ctx.state.mfaRequired; //false
50 | };
51 | } else {
52 | //inside this else statement means user is locally authenticated and with a secret,
53 | //to be redirected to verify MFA
54 | ctx.response.body = {
55 | successful: true,
56 | mfaRequired: ctx.state.mfaRequired; //true
57 | };
58 | };
59 | //sending the response that the request was successful
60 | ctx.response.status = 200;
61 | } else {
62 | //inside this else statement means user authentication with username/password failed
63 | ctx.response.body = {
64 | successful: false
65 | }
66 | ctx.response.status = 401;
67 | };
68 | return;
69 | };
70 |
71 | // Verification of MFA token code, if enabled
72 | MFARouter.post('/checkMFA', Bedrock.checkMFA, (ctx: Context) => {
73 | console.log('Successfully verified MFA code');
74 | ctx.response.redirect('/secret');
75 | return;
76 | });
77 |
78 | // Secret route with session verification middleware
79 | MFARouter.get('/secret', Bedrock.verifyAuth, (ctx: Context) => {
80 | console.log('Secret obtained!');
81 | ctx.response.body = 'Secret obtained!';
82 | ctx.response.status = 200;
83 | return;
84 | });
85 |
86 | // Route to log user out of server session
87 | MFARouter.get('/signout', Bedrock.signOut, (ctx: Context) => {
88 | console.log('Successfully signed out');
89 | ctx.response.body = 'Successfully signed out';
90 | ctx.response.status = 200;
91 | return;
92 | });
93 | ```
94 |
95 |
96 | ## OAuth 2.0 Strategy
97 | All OAuth providers require a client_id, client_secret, and redirect_uri. Additionally, Bedrock requires the developer to define scope for an added level of secruity. However, each OAuth provider publishes an extensive list of their supported scopes and they largely differ from each other. Please see our [documentation](https://bedrockauth.dev/docs) for more information about scopes for specific OAuth providers.
98 |
99 | ### Define your parameters
100 | ```typescript
101 | const params: OAuthParams = {
102 | provider: 'Github',
103 | client_id: Deno.env.get('CLIENT_ID')!,
104 | client_secret: Deno.env.get('CLIENT_SECRET')!,
105 | redirect_uri: Deno.env.get('AUTH_CALLBACK_URL')!,
106 | scope: 'read:user',
107 | };
108 | ```
109 | ### Initiate a Bedrock class
110 | ```typescript
111 | const Bedrock = init(params);
112 | ```
113 | ### Implant Bedrock middleware into your routes
114 |
115 | ```typescript
116 | // Route to redirect user to OAuth provider's login site
117 | OAuthRouter.get('/OAuth', Bedrock.sendRedirect);
118 |
119 | // Route to retrieve access token and create user session
120 | OAuthRouter.get('/OAuth/github', Bedrock.getToken, (ctx: Context) => {
121 | console.log('Successfully logged in via OAuth');
122 | ctx.response.redirect('/secret');
123 | return;
124 | });
125 |
126 | // Secret route with verification middleware
127 | OAuthRouter.get('/secret', Bedrock.verifyAuth, (ctx: Context) => {
128 | console.log('Secret obtained!');
129 | ctx.response.body = 'Secret obtained!';
130 | ctx.response.status = 200;
131 | return;
132 | });
133 |
134 | // Route to log user out of OAuth and server session
135 | OAuthRouter.get('/signout', Bedrock.signOut, (ctx: Context) => {
136 | console.log('Successfully signed out');
137 | ctx.response.redirect('/home');
138 | return;
139 | });
140 | ```
141 |
142 | ## How Bedrock is built
143 | - The timed one time password (TOTP) algorithm used in Bedrock follows the standard outlined in the [IETF RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238).
144 | - The SMS verification is provided through use of the [Twilio API](https://www.twilio.com/docs/usage/api).
145 | - The email verification is provided through [deno-mailer](https://deno.land/x/denomailer@1.0.1)
146 | ## How to Build Upon Bedrock
147 |
148 | #### How to contribute...
149 | - The first way to contribute is by giving us feedback with context about your use case. The will help us determine where we can improve for future builds.
150 | - Other ways to contribute would be to contact us or open issues on this repo. If neither of those options work for you, email us at bedrock.deno@gmail.com
151 | ## Authors
152 |
153 | - Eric Hagen: [Github](https://github.com/ejhagen) | [LinkedIn](https://www.linkedin.com/in/hagenforhire)
154 | - Anthony Valdez: [Github](https://github.com/va1dez) | [LinkedIn](https://www.linkedin.com/in/va1dez)
155 | - Julian Kang: [Github](https://github.com/julianswkang) | [LinkedIn](https://www.linkedin.com/in/julianswkang)
156 | - John Howell: [Github](https://github.com/Tak149) | [LinkedIn](https://www.linkedin.com/in/jdh3/)
157 |
158 | ## v1.0.3
159 | - Revision on auth logic for MFA
160 | - Updated additional variables to maintain camelCase consistency
161 |
162 | ## v1.0.2
163 | - Removed debugging information from Twilio class
164 | - Changed mfa_type to mfaType to maintain camelCase consistency
165 |
166 | ## v1.0.1
167 | - Added additional Local Authentication MFA option of e-mail (via [deno-mailer](https://deno.land/x/denomailer@1.0.1))
168 | - Added additional OAuth strategies, including Discord, Facebook, LinkedIn, and Twitter
169 |
170 | ## v1.0.0
171 |
172 | Initial release supporting the following authentication strategies:
173 | - Local Authentication, with optional MFA options
174 | - TOTP code (generated by popular apps such as Google and Microsoft Authenticator)
175 | - SMS code (Via Twilio)
176 | - OAuth
177 | - Github
178 | - Google
179 |
180 | Built on top of the [Oak](https://github.com/oakserver/oak) library and intended to be used as middleware in routes.
181 |
182 | Session management handled by [oak_sessions](https://github.com/jcs224/oak_sessions)
183 |
184 |
--------------------------------------------------------------------------------
/Strategies/Auth.ts:
--------------------------------------------------------------------------------
1 | import { Context } from "../deps.ts";
2 | import { LocalAuth } from "./MFA/LocalAuth.ts";
3 |
4 | export abstract class Auth {
5 | /**
6 | * @param ctx - Context object passed in via the Middleware chain
7 | * @param next - Invokes next function in the Middleware chain
8 | * Note: verifyAuth is an OPTIONAL authorization verifying middleware function for the developer to utilize
9 | This may be deferred, as developer may utilize different means of verifying authorization prior to allowing client access to sensitive material
10 | * verifyAuth checks the isLoggedIn and mfaSuccess session properties previously set by checkMFA
11 | * Will ensure that isLoggedIn is true
12 | Then will check to see if mfa_enabled is true -- if so, will ensure previous mfaCheck set property mfaSuccess to true
13 | If mfaSuccess is true, will then allow progression
14 | * Otherwise, if mfa_enabled is false, will also allow progression since mfaSuccess check is not warranted
15 | * If isLoggedIn is false, will return message stating client is "Not currently signed in"
16 | */
17 | readonly verifyAuth = async (ctx: Context, next: () => Promise) => {
18 | if (await ctx.state.session.has("isLoggedIn") && await ctx.state.session.get("isLoggedIn")) {
19 | if ( !(this instanceof LocalAuth) || this instanceof LocalAuth && (this.mfaType === undefined || await ctx.state.session.get('mfaSuccess') === true)){
20 | ctx.state.authSuccess = true;
21 | }
22 | } else {
23 | ctx.state.authSuccess = false;
24 | }
25 |
26 | return next();
27 | };
28 |
29 | /**
30 | *
31 | * @param ctx - Context object passed in via the Middleware chain
32 | * @param next - Invokes next function in the Middleware chain
33 | * Note: signOut is an OPTIONAL function for the developer to utilize in order to terminate the session. This may be deferred, as developer may utilize different means of verifying authorization prior to allowing client access to sensitive material.
34 | * signOut deletes the existing session instantiated via Oak_sessions and effectively signs the user out.
35 | */
36 | readonly signOut = async (ctx: Context, next: () => Promise) => {
37 | await ctx.state.session.deleteSession();
38 | next();
39 | };
40 | }
--------------------------------------------------------------------------------
/Strategies/MFA/LocalAuth.ts:
--------------------------------------------------------------------------------
1 | import { ClientOptions, Incoming, LocalAuthParams } from "../../types.ts";
2 | import { Context, decode64, SMTPClient, SendConfig } from "../../deps.ts";
3 | import { generateTOTP } from "./totp.ts";
4 | import { Twilio } from "./twilio.ts";
5 | import { Auth } from "../Auth.ts";
6 |
7 | /**
8 | * Local Authentication class with Middleware functions to provide functionality needed by the developer
9 | */
10 | export class LocalAuth extends Auth {
11 | checkCreds: (username: string, password: string) => Promise;
12 | getSecret?: (username: string) => Promise;
13 | readCreds?: (ctx: Context) => Promise;
14 | mfaType?: string;
15 | accountSID?: string;
16 | authToken?: string;
17 | getNumber?: (username: string) => Promise;
18 | sourceNumber?: string;
19 | clientOptions?: ClientOptions;
20 | getEmail?: (username: string) => Promise;
21 | fromAddress?: string;
22 |
23 | constructor(stratParams: LocalAuthParams) {
24 | super();
25 | this.checkCreds = stratParams.checkCreds;
26 |
27 | Object.assign(this, stratParams);
28 | }
29 |
30 | /**
31 | * @param context object
32 | * @param next function
33 | *
34 | * The localLogin method leverages the checkCreds property that was initialized when the object was instantiated
35 | * It may also utilize the optional readCreds property that the developer may use in order to provide an array of [username, password]
36 | * If readCreds is not initialized, will assume the developer is utilizing the Authorization header Basic to pass username and password in base64
37 |
38 | * Will leverage the checkCreds property with the passed in username and password.
39 | If returns true, will set session 'isLoggedIn' to true and initialize state property 'localVerified' to true.
40 | Else, will set 'isLoggedIn' to false and initialize state property 'localVerified' to false.
41 |
42 | * Developer may then leverage the localVerified property on state in subsequent middleware
43 | */
44 | readonly localLogin = async (ctx: Context, next: () => Promise) => {
45 | let credentials: string[] = [];
46 |
47 | if (this.readCreds === undefined) {
48 | if (ctx.request.headers.has("Authorization")) {
49 | let authHeaders: string = ctx.request.headers.get("Authorization")!;
50 | if (authHeaders.startsWith("Basic ")) {
51 | authHeaders = authHeaders.slice(6);
52 | }
53 | const auth = decode64(authHeaders);
54 | const decodedAuth = new TextDecoder().decode(auth!);
55 | credentials = decodedAuth.split(":");
56 | }
57 | } else {
58 | credentials = await this.readCreds(ctx);
59 | }
60 |
61 | const [username, password] = [credentials[0], credentials[1]];
62 | await ctx.state.session.set("username", username);
63 |
64 | if (await this.checkCreds(username, password)) {
65 | await ctx.state.session.set("isLoggedIn", true);
66 | await this.sendMFA(ctx);
67 |
68 | ctx.state.localVerified = true;
69 | ctx.state.mfaRequired = this.mfaType !== undefined;
70 | } else {
71 | await ctx.state.session.set("isLoggedIn", false);
72 | ctx.state.localVerified = false;
73 | }
74 |
75 | return next();
76 | };
77 |
78 | /**
79 | * @param context object
80 | * @param next function
81 | * Invoking checkMFA will utilize the object's getSecret property (a function defined by the developer)
82 | * This will utilize the Oak session to obtain the client username in order to utilize getSecret
83 | * Upon obtaining the username's associated secret, will invoke the imported generateTOTP() function
84 | * Checks to see if the input code from client matches the generated TOTP
85 | * Note: the developer will need to ensure that the client's MFA input is passed into the
86 | context.request body as the property, 'code'
87 | * If verified, will initialize session 'mfaSuccess' and add mfaVerified property on ctx.state as set to true. Else, will initialize to false
88 | The developer can use ctx.state.mfaVerified to determine if client's mfa check was successful or not
89 | */
90 | readonly checkMFA = async (ctx: Context, next: () => Promise) => {
91 | const body = await ctx.request.body();
92 | const bodyValue = await body.value;
93 | const mfaSecret = await this.getSecret!(
94 | await ctx.state.session.get("username"),
95 | );
96 |
97 | const currentTOTP = await generateTOTP(mfaSecret!);
98 |
99 | const verified = currentTOTP.some((totp) => {
100 | return totp === bodyValue.code;
101 | });
102 |
103 | if (verified) {
104 | await ctx.state.session.set("mfaSuccess", true);
105 | ctx.state.mfaVerified = true;
106 | return next();
107 | } else {
108 | await ctx.state.session.set("mfaSuccess", false);
109 | ctx.state.mfaVerified = false;
110 | return next();
111 | }
112 | };
113 | /**
114 | * @param context object
115 | * Invoking sendMFA will send a 6 digit code via one of two methods: SMS or e-mail
116 | * Will check to see which mfaType is initialized
117 | * If mfaType is SMS, will instantiate an object from the Twilio class with the accountSID and authtoken provided from Twilio
118 | * Will then invoke the Twilio method's sendSMS function while passing in an object that designates
119 | the 'From' phone number (developer's designated Twilio phone number) and 'To' phone number (client/user's phone number)
120 | */
121 | readonly sendMFA = async (ctx: Context) => {
122 | const secret = await this.getSecret!(
123 | await ctx.state.session.get("username"),
124 | );
125 |
126 | ctx.state.hasSecret = (secret === null) ? false : true;
127 |
128 | if (this.mfaType === "SMS" && secret) {
129 | const sms = new Twilio(this.accountSID!, secret, this.authToken!);
130 | const context: Incoming = {
131 | From: this.sourceNumber!,
132 | To: await this.getNumber!(await ctx.state.session.get("username"))!,
133 | };
134 |
135 | await sms.sendSms(context);
136 | } else if (this.mfaType === "Email" && secret) {
137 | // Generate TOTP code
138 | const code = await generateTOTP(secret);
139 |
140 | // Set up email client
141 | const client = new SMTPClient(this.clientOptions!);
142 |
143 | // Build email
144 | const subjectText: string = "Your MFA code is " + code[1];
145 | const contentText: string = subjectText;
146 | const htmlText: string = "Your MFA code is " + code[1] + ".
";
147 | const userEmail = await this.getEmail!(await ctx.state.session.get('username'));
148 |
149 | const newEmail: SendConfig = {
150 | from: this.fromAddress!,
151 | to: userEmail,
152 | subject: subjectText,
153 | content: contentText,
154 | html: htmlText,
155 | };
156 |
157 | // Send email and close server connection
158 | await client.send(newEmail);
159 | await client.close();
160 | }
161 | return;
162 | };
163 |
164 | static readonly generateTOTPSecret = (): string => {
165 | const randString: Array = new Array(32);
166 | const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
167 |
168 | for (let i = 0; i < randString.length; i++) {
169 | randString[i] = base32Chars[Math.floor(Math.random() * 32)];
170 | }
171 |
172 | return randString.join('');
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/Strategies/MFA/totp.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Modern implementation of the TOTP algorithm using the new Crypto module
3 | * in Deno as well as a pseudorandom secret generator in base32
4 | */
5 |
6 | import { crypto } from "../../deps.ts";
7 | import { decode32 } from "../../deps.ts";
8 |
9 | /**
10 | * HMAC SHA-1 function used to hash message based off key
11 | * @param k - Key passed in as a UInt8Array
12 | * @param m - Message passed in as a UInt8Array
13 | * @returns HMAC SHA1 hashed message returned as a UInt8Array
14 | */
15 | async function hmacSHA1(k: Uint8Array, m: Uint8Array): Promise {
16 | // SHA1 has a block size of 64 bytes
17 | const BLOCKSIZE = 64;
18 |
19 | // Helper function to return a key that is equal to the above block size
20 | async function blocksizeKey (key: Uint8Array) {
21 | if (key.length > BLOCKSIZE) {
22 | key = new Uint8Array(await crypto.subtle.digest('SHA-1', key));
23 | }
24 |
25 | let output = new Uint8Array(BLOCKSIZE);
26 |
27 | if (key.length < BLOCKSIZE) {
28 | for (let i = 0; i < BLOCKSIZE; i++) {
29 | output[i] = (i < key.length) ? key[i] : 0;
30 | }
31 | } else {
32 | output = key;
33 | }
34 |
35 | return output;
36 | }
37 |
38 | // Computing the block sized key, padded with 0s and hashed with SHA-1
39 | const blockKey = await blocksizeKey(k);
40 |
41 | // Creation of outer and inner padded keys followed by bitwise XOR transformation
42 | const outer_pkey = new Uint8Array(BLOCKSIZE);
43 | const inner_pkey = new Uint8Array(BLOCKSIZE);
44 |
45 | for (let i = 0; i < BLOCKSIZE; i++) {
46 | outer_pkey[i] = blockKey[i] ^ 0x5c;
47 | inner_pkey[i] = blockKey[i] ^ 0x36;
48 | }
49 |
50 | // Hash of concat of ipad and message
51 | let firstPass = new Uint8Array(inner_pkey.length + m.length);
52 | firstPass.set(inner_pkey);
53 | firstPass.set(m, inner_pkey.length);
54 | firstPass = new Uint8Array(await crypto.subtle.digest('SHA-1', firstPass));
55 |
56 | // Hash of concat of opad and above hash
57 | let result = new Uint8Array(outer_pkey.length + firstPass.length);
58 | result.set(outer_pkey);
59 | result.set(firstPass, outer_pkey.length);
60 | result = new Uint8Array(await crypto.subtle.digest('SHA-1', result));
61 |
62 | return result;
63 | }
64 |
65 | /**
66 | * Function that generates array of TOTP codes
67 | * @param secret - Base32 secret required by TOTP algorithm
68 | * @param numTimeSteps - Time steps derived by dividing Unix Epoch by 30 seconds (time window of each code)
69 | * @returns Array of TOTP codes, with first element being previous token, second being current, and third being next
70 | */
71 | export async function generateTOTP(secret:string, numTimeSteps?: number): Promise {
72 | // In place in order to faciliate testing
73 | if (numTimeSteps === undefined) {
74 | // Recommended timestep based off RFC6238 is 30 seconds
75 | const TIMESTEP = 30;
76 |
77 | // Determine number of steps based off dividing the current Unix time by the timestep interval
78 | numTimeSteps = Math.floor(Math.round((new Date()).getTime() / 1000)/TIMESTEP);
79 | }
80 |
81 | // Generates TOTP based off current UNIX time - broken into function in order to invoke and return
82 | // array of 3 values, token before, during, and after
83 | async function TOTP(timeSteps: number): Promise {
84 | // Convert the integer value of number of steps into a hexadecimal string
85 | const hexTime = timeSteps.toString(16);
86 |
87 | // Padding hexTime with leading 0s to fit 16 character requirement
88 | let hexMod = '';
89 |
90 | for (let i = 0; i < 16 - hexTime.length; i++) {
91 | hexMod += '0';
92 | }
93 |
94 | // Add zeros to front of hexTime
95 | hexMod += hexTime;
96 |
97 | // Split time string to hex components, asserted to not be null
98 | const splitString = hexMod.match(/.{1,2}/g)!;
99 |
100 | // Create new output array equal to the final length of input (should be 16 if used with SHA1)
101 | const decArray = new Array(splitString.length);
102 |
103 | // Parse each block from hex to decimal
104 | for (let i = 0; i < decArray.length; i++) {
105 | decArray[i] = parseInt(splitString[i], 16);
106 | }
107 |
108 | // Translate decimal array to Uint8Array for ingestion by HMAC-SHA1
109 | const timeHex = new Uint8Array(decArray);
110 |
111 | // Returns an error string if secret is not Base32
112 | const regex = /^([A-Z2-7=]{8})+$/
113 | if (!regex.test(secret)) {
114 | throw new Error('Not a base32 secret');
115 | }
116 |
117 | // Decode the secret from base32 to a binary Uint8Array to prepare for HMAC-SHA1
118 | const binaryData = decode32(secret);
119 |
120 | // Calculate HMAC-SHA1 hash of the secret and the current time
121 | const hmac = await hmacSHA1(binaryData, timeHex);
122 |
123 | // Obtain the last hash byte, required for TOTP algorithm
124 | const last_hash_byte = hmac[hmac.length-1];
125 |
126 | // Obtain offset value by using Bitwise AND again 0x0f
127 | const offset = last_hash_byte & 0x0f;
128 |
129 | // Generate token code using Bitwise AND and performing a left shift based off values
130 | // specified in the TOTP algorithm
131 | let code = 0;
132 | code = code | ((hmac[offset] & 0x7f) << 24);
133 | code = code | ((hmac[offset + 1] & 0xff) << 16);
134 | code = code | ((hmac[offset + 2] & 0xff) << 8);
135 | code = code | (hmac[offset + 3] & 0xff);
136 |
137 | code = code % 1000000;
138 |
139 | // String manipulation and conversion to account for leading zeros in TOTP code
140 | let output = code.toString();
141 |
142 | for (let i = 0, length = output.length; i < 6 - length; i++) {
143 | output = '0'.concat(output);
144 | }
145 |
146 | return output;
147 | }
148 |
149 | // Defining array to hold TOTP [token 1 step prior, current, and 1 step ahead], then
150 | // generating tokens
151 | const result = [];
152 |
153 | for (let i = -1; i < 2; i++) {
154 | result.push(await TOTP(numTimeSteps - i));
155 | }
156 |
157 | return result;
158 | }
--------------------------------------------------------------------------------
/Strategies/MFA/twilio.ts:
--------------------------------------------------------------------------------
1 | import { encode64 } from './../../deps.ts';
2 | import { generateTOTP } from './totp.ts'
3 | import { SMSRequest, Incoming } from './../../types.ts'
4 |
5 | /**
6 | * TwilioSMS class requires 3 passed in properties:
7 | * AccountSID and AuthToken (provided by Twilio upon account creation)
8 | * Secret - secret associated with with the username
9 | */
10 | export class Twilio {
11 | public readonly authorizationHeader: string;
12 |
13 | constructor(private accountSID: string, private secret: string, private authToken: string) {
14 | //building the basic access authentication header that must be sent with every HTTP request to the Twilio API, which requires base64 encoding
15 | this.authorizationHeader = 'Basic ' + encode64(`${accountSID}:${authToken}`);
16 | }
17 |
18 | /**
19 | * @param payload - SMSRequest object that contains information such as From, To, and Body of the SMS message
20 | * @returns Body of the API response, which will be the code sent to the end user
21 | *
22 | * Sends a post request to the TwilioSMS API
23 | * Content-type of the SMS message is passed as url-encoded form (ex. key1=value1&key2=value2)
24 | * postSMSRequest utilizes the authorizationHeader property as the authorization header
25 | * The content of the SMS message (payload) is passed within the body after invoking the URLSearchParams function
26 | */
27 | private async postSMSRequest(payload: SMSRequest): Promise {
28 | //perform HTTP post request to the https://api.twilio.com/2010-04-01/Accounts/YOUR_ACC_SID/Messages.json URI to place the send SMS request
29 |
30 | const data = await fetch(
31 | 'https://api.twilio.com/2010-04-01/Accounts/' + this.accountSID + '/Messages.json',
32 | {
33 | method: 'POST',
34 | headers: {
35 | 'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8',
36 | Authorization: this.authorizationHeader,
37 | },
38 | body: new URLSearchParams(payload),
39 | }
40 | )
41 | const response = await data.json();
42 | const { body } = response;
43 | //returning only the body of the response object, which is the code that was sent to the end-user
44 | return body;
45 | }
46 | /**
47 | *
48 | * @param fromAndTo - an object with the To and From phone number for the Twilio API to send the message
49 | *
50 | * Will invoke the imported generateTOTP() function with the passed in secret
51 | * Since generateTOTP provides an array of 3 codes, will utilize the code at index 1
52 | * Will invoke the postSMSRequest function with the passed in 'From', 'To', and newly generated code as the newPayload
53 | */
54 | public async sendSms(fromAndTo: Incoming){
55 | const code = await generateTOTP(this.secret);
56 | const messageBody:string = code[1];
57 | const {From, To} = fromAndTo;
58 | const newPayload: SMSRequest = {
59 | From,
60 | To,
61 | Body: messageBody
62 | }
63 | return this.postSMSRequest(newPayload);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Strategies/OAuth/DiscordOAuth.ts:
--------------------------------------------------------------------------------
1 | import { Context, helpers } from "../../deps.ts";
2 | import { OAuthParams } from "../../types.ts"
3 | import { OAuth } from './OAuth.ts';
4 |
5 | export class DiscordOAuth extends OAuth{
6 | constructor(stratParams: OAuthParams) {
7 | super(stratParams);
8 | }
9 |
10 | /**
11 | * Appends necessary client info onto uri string and redirects to generated link.
12 | * @param ctx - Context object passed in via the Middleware chain
13 | **/
14 | sendRedirect = async (ctx: Context): Promise => {
15 | let uri = this.uriBuilder();
16 | await ctx.state.session.set("state", this.randomStringGenerator(20));
17 |
18 | uri += `&state=${await ctx.state.session.get('state')}`;
19 |
20 | ctx.response.redirect(uri);
21 | return;
22 | };
23 |
24 | /**
25 | * Functionality to generate post request to Discord server to obtain access token
26 | * @param ctx - Context object passed in via the Middleware chain
27 | * @param next - Invokes next function in the Middleware chain
28 | **/
29 | getToken = async (ctx: Context, next: () => Promise) => {
30 | try {
31 | const params = helpers.getQuery(ctx, { mergeParams: true });
32 | const { code, state } = params;
33 |
34 | if (params.error) throw new Error('User did not authorize app');
35 |
36 | if (state !== await ctx.state.session.get("state")) {
37 | throw new Error('State validation on incoming response failed');
38 | }
39 |
40 | const response = await fetch("https://discord.com/api/oauth2/token", {
41 | method: "POST",
42 | headers: {
43 | "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
44 | },
45 | body: new URLSearchParams({
46 | client_id: this.client_id,
47 | client_secret: this.client_secret,
48 | code,
49 | grant_type: 'authorization_code',
50 | redirect_uri: this.redirect_uri
51 | }),
52 | });
53 |
54 | const token = await response.json();
55 |
56 | if (response.status !== 200) {
57 | console.log('Failed Response Body: ', token);
58 | throw new Error('Unsuccessful authentication response');
59 | }
60 |
61 | // Bedrock session management variable assignment
62 | await ctx.state.session.set("accessToken", token.access_token);
63 | await ctx.state.session.set("isLoggedIn", true);
64 |
65 | /**
66 | * State properties that expire at end of response cycle
67 | * Meant for developer to utilize in case of external session management
68 | * Token passed to expose access/refresh token to developer
69 | **/
70 | ctx.state.OAuthVerified = true;
71 | ctx.state.token = token;
72 | }
73 | catch(err) {
74 | await ctx.state.session.set("isLoggedIn", false);
75 | ctx.state.OAuthVerified = false;
76 |
77 | console.log('There was a problem logging in with Discord: ', err)
78 | }
79 |
80 | return next();
81 | };
82 | }
--------------------------------------------------------------------------------
/Strategies/OAuth/FacebookOAuth.ts:
--------------------------------------------------------------------------------
1 | import { Context, helpers } from "../../deps.ts";
2 | import { OAuthParams } from "../../types.ts"
3 | import { OAuth } from './OAuth.ts';
4 |
5 | export class FacebookOAuth extends OAuth{
6 | constructor(stratParams: OAuthParams) {
7 | super(stratParams);
8 | }
9 |
10 | /**
11 | * Appends necessary client info onto uri string and redirects to generated link.
12 | * @param ctx - Context object passed in via the Middleware chain
13 | **/
14 | sendRedirect = async (ctx: Context): Promise => {
15 | let uri = this.uriBuilder();
16 | await ctx.state.session.set("state", this.randomStringGenerator(20));
17 |
18 | uri += `&state=${await ctx.state.session.get('state')}`;
19 |
20 | ctx.response.redirect(uri);
21 | return;
22 | };
23 |
24 | /**
25 | * Functionality to generate post request to Facebook server to obtain access token
26 | * @param ctx - Context object passed in via the Middleware chain
27 | * @param next - Invokes next function in the Middleware chain
28 | **/
29 | getToken = async (ctx: Context, next: () => Promise) => {
30 | try {
31 | const params = helpers.getQuery(ctx, { mergeParams: true });
32 | const { code, state } = params;
33 |
34 | if (params.error) throw new Error('User did not authorize app');
35 |
36 | if (state !== await ctx.state.session.get("state")) {
37 | throw new Error('State validation on incoming response failed');
38 | }
39 |
40 | const response = await fetch("https://graph.facebook.com/v13.0/oauth/access_token?", {
41 | method: "POST",
42 | headers: {
43 | "Accept": "application/json",
44 | "Content-Type": "application/json",
45 | },
46 | body: new URLSearchParams({
47 | client_id: this.client_id,
48 | redirect_uri: this.redirect_uri,
49 | client_secret: this.client_secret,
50 | code,
51 | }),
52 | });
53 |
54 | const token = await response.json();
55 |
56 | if (response.status !== 200) {
57 | console.log('Failed Response Body: ', token);
58 | throw new Error('Unsuccessful authentication response');
59 | }
60 |
61 | // Bedrock session management variable assignment
62 | await ctx.state.session.set("accessToken", token.access_token);
63 | await ctx.state.session.set("isLoggedIn", true);
64 |
65 | /**
66 | * State properties that expire at end of response cycle
67 | * Meant for developer to utilize in case of external session management
68 | * Token passed to expose access/refresh token to developer
69 | **/
70 | ctx.state.OAuthVerified = true;
71 | ctx.state.token = token;
72 | }
73 | catch(err) {
74 | await ctx.state.session.set("isLoggedIn", false);
75 | ctx.state.OAuthVerified = false;
76 |
77 | console.log('There was a problem logging in with Facebook: ', err)
78 | }
79 |
80 | return next();
81 | };
82 | }
--------------------------------------------------------------------------------
/Strategies/OAuth/GithubOAuth.ts:
--------------------------------------------------------------------------------
1 | import { Context, helpers } from "../../deps.ts";
2 | import { OAuthParams } from "../../types.ts";
3 | import { OAuth } from './OAuth.ts';
4 |
5 | export class GithubOAuth extends OAuth {
6 | constructor(stratParams: OAuthParams) {
7 | super(stratParams)
8 | }
9 |
10 | /**
11 | * Appends necessary client info onto uri string and redirects to generated link.
12 | * @param ctx - Context object passed in via the Middleware chain
13 | **/
14 | sendRedirect = async (ctx: Context): Promise => {
15 | let uri = this.uriBuilder();
16 |
17 | const state = this.randomStringGenerator(20);
18 |
19 | await ctx.state.session.flash("state", state);
20 |
21 | uri += `&state=${state}`;
22 |
23 | ctx.response.redirect(uri);
24 | return;
25 | };
26 |
27 | /**
28 | * Functionality to generate post request to Github server to obtain access token
29 | * @param ctx - Context object passed in via the Middleware chain
30 | * @param next - Invokes next function in the Middleware chain
31 | **/
32 | getToken = async (ctx: Context, next: () => Promise) => {
33 | try {
34 | const params = helpers.getQuery(ctx, { mergeParams: true });
35 | const { code, state } = params;
36 |
37 | if (params.error) throw new Error('User did not authorize app');
38 |
39 | if (state !== await ctx.state.session.get('state')) {
40 | throw new Error('State validation on incoming response failed');
41 | }
42 |
43 | const response = await fetch("https://github.com/login/oauth/access_token", {
44 | method: "POST",
45 | headers: {
46 | "Content-Type": "application/json",
47 | "Accept" : "application/json"
48 | },
49 | body: JSON.stringify({
50 | client_id: this.client_id,
51 | client_secret: this.client_secret,
52 | code
53 | }),
54 | });
55 |
56 | const token = await response.json();
57 |
58 | if (response.status !== 200) {
59 | console.log('Failed Response Body: ', token);
60 | throw new Error('Unsuccessful authentication response');
61 | }
62 |
63 | // Bedrock session management variable assignment
64 | await ctx.state.session.set("accessToken", token.access_token);
65 | await ctx.state.session.set("isLoggedIn", true);
66 |
67 | /**
68 | * State properties that expire at end of response cycle
69 | * Meant for developer to utilize in case of external session management
70 | * Token passed to expose access/refresh token to developer
71 | **/
72 | ctx.state.OAuthVerified = true;
73 | ctx.state.token = token;
74 | }
75 | catch(err) {
76 | await ctx.state.session.set("isLoggedIn", false);
77 | ctx.state.OAuthVerified = false;
78 |
79 | console.log('There was a problem logging in with Github: ', err)
80 | }
81 |
82 | await next();
83 | };
84 | }
85 |
--------------------------------------------------------------------------------
/Strategies/OAuth/GoogleOAuth.ts:
--------------------------------------------------------------------------------
1 | import { Context, helpers } from "../../deps.ts";
2 | import { OAuthParams } from "../../types.ts";
3 | import { OAuth } from './OAuth.ts'
4 |
5 | export class GoogleOAuth extends OAuth{
6 | protected readonly access_type?: 'online' | 'offline';
7 | protected readonly prompt?: 'none' | 'consent' | 'select_account';
8 |
9 | constructor(stratParams: OAuthParams) {
10 | super(stratParams);
11 | Object.assign(this, stratParams)!;
12 | }
13 |
14 | /**
15 | * Appends necessary client info onto uri string and redirects to generated link.
16 | * @param ctx - Context object passed in via the Middleware chain
17 | **/
18 | sendRedirect = async (ctx: Context): Promise => {
19 | let uri = this.uriBuilder();
20 | await ctx.state.session.set('state', this.randomStringGenerator(20));
21 |
22 | uri += `&state=${await ctx.state.session.get('state')}`;
23 |
24 | if(this.prompt !== undefined){
25 | uri += `&prompt=${this.prompt}`;
26 | }
27 | if(this.access_type !== undefined){
28 | uri += `&access_type=${this.access_type}`;
29 | }
30 |
31 | ctx.response.redirect(uri);
32 | return;
33 | }
34 |
35 | /**
36 | * Functionality to generate post request to Google server to obtain access token
37 | * @param ctx - Context object passed in via the Middleware chain
38 | * @param next - Invokes next function in the Middleware chain
39 | **/
40 | getToken = async ( ctx: Context, next: () => Promise) => {
41 | try {
42 | const params = helpers.getQuery(ctx, { mergeParams: true });
43 | const { code, state } = params;
44 |
45 | if (params.error) throw new Error('User did not authorize app');
46 |
47 | if (state !== await ctx.state.session.get('state')) {
48 | throw new Error('State validation on incoming response failed');
49 | }
50 |
51 | const response = await fetch('https://oauth2.googleapis.com/token', {
52 | method: "POST",
53 | headers: {
54 | "Accept": "application/json",
55 | "Content-Type": "application/json",
56 | },
57 | body: JSON.stringify({
58 | client_id: this.client_id,
59 | client_secret: this.client_secret,
60 | code,
61 | grant_type: "authorization_code",
62 | redirect_uri: this.redirect_uri,
63 | }),
64 | });
65 |
66 | const token = await response.json();
67 |
68 | if (response.status !== 200) {
69 | console.log('Failed Response Body: ', token);
70 | throw new Error('Unsuccessful authentication response');
71 | }
72 |
73 | // Bedrock session management variable assignment
74 | await ctx.state.session.set('accessToken', token.access_token);
75 | await ctx.state.session.set('isLoggedIn', true);
76 |
77 | /**
78 | * State properties that expire at end of response cycle
79 | * Meant for developer to utilize in case of external session management
80 | * Token passed to expose access/refresh token to developer
81 | **/
82 |
83 | ctx.state.OAuthVerified = true;
84 | ctx.state.token = token;
85 | }
86 | catch(err) {
87 | await ctx.state.session.set("isLoggedIn", false);
88 | ctx.state.OAuthVerified = false;
89 |
90 | console.log('There was a problem logging in with Google: ', err);
91 | }
92 |
93 | return next();
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/Strategies/OAuth/LinkedinOAuth.ts:
--------------------------------------------------------------------------------
1 | import { Context, helpers } from "../../deps.ts";
2 | import { OAuthParams } from "../../types.ts";
3 | import { OAuth } from './OAuth.ts';
4 |
5 | export class LinkedinOAuth extends OAuth{
6 | constructor(stratParams: OAuthParams) {
7 | super(stratParams);
8 | }
9 |
10 | /**
11 | * Appends necessary client info onto uri string and redirects to generated link.
12 | * @param ctx - Context object passed in via the Middleware chain
13 | **/
14 | sendRedirect = async (ctx: Context): Promise => {
15 | let uri = this.uriBuilder();
16 | await ctx.state.session.set("state", this.randomStringGenerator(20));
17 |
18 | uri += `&state=${await ctx.state.session.get("state")}`;
19 |
20 | ctx.response.redirect(uri);
21 | return;
22 | };
23 |
24 | /**
25 | * Functionality to generate post request to LinkedIn server to obtain access token
26 | * @param ctx - Context object passed in via the Middleware chain
27 | * @param next - Invokes next function in the Middleware chain
28 | **/
29 | getToken = async (ctx: Context, next: () => Promise) => {
30 | try {
31 | const params = helpers.getQuery(ctx, { mergeParams: true });
32 | const { code, state } = params;
33 |
34 | if (params.error) throw new Error('User did not authorize app');
35 |
36 | if (state !== await ctx.state.session.get("state")) {
37 | throw new Error('State validation on incoming response failed');
38 | }
39 |
40 | const response = await fetch("https://www.linkedin.com/oauth/v2/accessToken", {
41 | method: "POST",
42 | headers: {
43 | "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
44 | },
45 | body: new URLSearchParams({
46 | client_id: this.client_id,
47 | client_secret: this.client_secret,
48 | code,
49 | grant_type: 'authorization_code',
50 | redirect_uri: this.redirect_uri,
51 | }),
52 | });
53 |
54 | const token = await response.json();
55 |
56 | if (response.status !== 200) {
57 | console.log('Failed Response Body: ', token);
58 | throw new Error('Unsuccessful authentication response');
59 | }
60 |
61 | // Bedrock session management variable assignment
62 | await ctx.state.session.set("accessToken", token.access_token);
63 | await ctx.state.session.set("isLoggedIn", true);
64 |
65 | /**
66 | * State properties that expire at end of response cycle
67 | * Meant for developer to utilize in case of external session management
68 | * Token passed to expose access/refresh token to developer
69 | **/
70 | ctx.state.OAuthVerified = true;
71 | ctx.state.token = token;
72 | }
73 | catch (err) {
74 | await ctx.state.session.set("isLoggedIn", false);
75 | ctx.state.OAuthVerified = false;
76 |
77 | console.log('There was a problem logging in with LinkedIn: ', err)
78 | }
79 |
80 | return next();
81 | };
82 | }
--------------------------------------------------------------------------------
/Strategies/OAuth/OAuth.ts:
--------------------------------------------------------------------------------
1 | import { OAuthParams } from "../../types.ts";
2 | import { Auth } from "../Auth.ts";
3 |
4 | export abstract class OAuth extends Auth{
5 | /**
6 | * Universal OAuth properties and constructor method
7 | */
8 |
9 | // deno-lint-ignore no-explicit-any
10 | [key: string]: any;
11 | protected readonly provider: string;
12 | protected readonly client_id: string;
13 | protected readonly client_secret: string;
14 | protected readonly redirect_uri: string;
15 | protected readonly response_type = 'code';
16 | protected readonly scope: string;
17 | protected readonly URIprops: string[] = ['client_id', 'redirect_uri', 'scope', 'response_type'];
18 |
19 | constructor(stratParams: OAuthParams) {
20 | super();
21 | this.client_id = stratParams.client_id;
22 | this.client_secret = stratParams.client_secret;
23 | this.redirect_uri = stratParams.redirect_uri;
24 | this.scope = stratParams.scope;
25 | this.provider = stratParams.provider;
26 | }
27 |
28 | /**
29 | * Universal class methods used across all OAuth classes
30 | */
31 |
32 | /**
33 | * Pseudorandom string generator used to generate various types of secrets across the library
34 | * @param length - The length of the string generated
35 | * @returns {string} String of length passed in to the function
36 | */
37 | randomStringGenerator = (length: number): string => {
38 | let result = '';
39 | const alphanum = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
40 | for (let i = 0; i < length; i++) {
41 | result += alphanum[Math.floor(Math.random() * alphanum.length)];
42 | }
43 | return result;
44 | }
45 |
46 | /**
47 | * Generic URI builder function to create the OAuth link based off provider
48 | * @returns {string} Initial URI of the respective OAuth provider
49 | */
50 | uriBuilder = ():string => {
51 | let uri;
52 |
53 | switch (this.provider) {
54 | case 'Discord':
55 | uri = 'https://discord.com/api/oauth2/authorize?';
56 | break;
57 | case 'Google':
58 | uri = 'http://accounts.google.com/o/oauth2/v2/auth?';
59 | break;
60 | case 'Github':
61 | uri = 'http://github.com/login/oauth/authorize?';
62 | break;
63 | case 'Linkedin':
64 | uri = 'https://www.linkedin.com/oauth/v2/authorization?';
65 | break;
66 | case 'Facebook':
67 | uri = 'https://www.facebook.com/v13.0/dialog/oauth?';
68 | break;
69 | case 'Twitter':
70 | uri = 'https://twitter.com/i/oauth2/authorize?';
71 | break;
72 | default:
73 | throw new Error(
74 | 'Invalid provider was provided.'
75 | );
76 | }
77 |
78 | for (const prop of this.URIprops){
79 | if (this[prop] !== undefined) {
80 | uri += `${prop}=${this[prop]}&`;
81 | }
82 | }
83 | uri = uri.slice(0, uri.length - 1);
84 | return uri;
85 | }
86 | }
--------------------------------------------------------------------------------
/Strategies/OAuth/TwitterOAuth.ts:
--------------------------------------------------------------------------------
1 | import { Context, crypto, encode64url, helpers, encode64, } from "./../../deps.ts";
2 | import { OAuthParams } from "./../../types.ts";
3 | import { OAuth } from './OAuth.ts';
4 |
5 | /**
6 | * Appends necessary client info onto uri string and redirects to generated link.
7 | * @param ctx
8 | * @returns
9 | **/
10 |
11 | export class TwitterOAuth extends OAuth{
12 | constructor(stratParams: OAuthParams) {
13 | super(stratParams);
14 | Object.assign(this, stratParams)!;
15 | }
16 |
17 | /**
18 | * Appends necessary client info onto uri string and redirects to generated link. Utilizes PKCE SHA256 to secure response
19 | * and prevent malicious applications on device to steal access token
20 | * @param ctx - Context object passed in via the Middleware chain
21 | **/
22 | sendRedirect = async (ctx: Context, next: () => Promise): Promise => {
23 | let uri = this.uriBuilder();
24 |
25 | const state = this.randomStringGenerator(20);
26 | const code_challenge = this.randomStringGenerator(128);
27 |
28 | await ctx.state.session.flash('state', state);
29 | await ctx.state.session.flash('code_challenge', code_challenge);
30 |
31 | const challengeArr = new TextEncoder().encode(code_challenge);
32 | const encoded = encode64url(
33 | await crypto.subtle.digest("SHA-256", challengeArr),
34 | );
35 |
36 | uri += `&state=${state}&code_challenge=${encoded}&code_challenge_method=S256`;
37 |
38 | ctx.response.redirect(uri);
39 | await next();
40 | return;
41 | };
42 |
43 | /**
44 | * Functionality to generate post request to Twitter server to obtain access token. Utilizes PKCE SHA256 to secure response
45 | * and prevent malicious applications on device to steal access token
46 | * @param ctx - Context object passed in via the Middleware chain
47 | * @param next - Invokes next function in the Middleware chain
48 | **/
49 | getToken = async (ctx: Context, next: () => Promise) => {
50 | try {
51 | const params = helpers.getQuery(ctx, { mergeParams: true });
52 | const { code, state } = params;
53 |
54 | const sessionState = await ctx.state.session.get('state');
55 |
56 | if (params.error) throw new Error('User did not authorize app');
57 |
58 | if (state !== sessionState) {
59 | throw new Error('State validation on incoming response failed');
60 | }
61 |
62 | const authHeader = encode64(`${this.client_id}:${this.client_secret}`);
63 |
64 | const response = await fetch(
65 | "https://api.twitter.com/2/oauth2/token",
66 | {
67 | method: "POST",
68 | headers: {
69 | "Content-Type": "application/x-www-form-urlencoded",
70 | "Charset": "UTF-8",
71 | "Authorization": 'Basic ' + authHeader,
72 | },
73 | body: new URLSearchParams({
74 | client_id: this.client_id,
75 | code_verifier: await ctx.state.session.get('code_challenge'),
76 | code,
77 | grant_type: "authorization_code",
78 | redirect_uri: this.redirect_uri
79 | }),
80 | },
81 | );
82 |
83 | const token = await response.json();
84 | if (response.status !== 200) {
85 | console.log('Failed Response Body: ', token);
86 | throw new Error('Unsuccessful authentication response')
87 | }
88 |
89 | await ctx.state.session.set("accessToken", token.access_token);
90 | await ctx.state.session.set("isLoggedIn", true);
91 |
92 | ctx.state.OAuthVerified = true;
93 | ctx.state.token = token;
94 | }
95 | catch(err){
96 | await ctx.state.session.set("isLoggedIn", false);
97 | ctx.state.OAuthVerified = false;
98 |
99 | console.log('There was a problem logging in with Twitter: ', err);
100 | }
101 | await next();
102 | };
103 |
104 | }
105 |
--------------------------------------------------------------------------------
/__tests__/DiscordOAuth-test.ts:
--------------------------------------------------------------------------------
1 | import { describe, assertEquals } from "../deps.ts";
2 | import { init } from "../mod.ts";
3 | import { DiscordOAuth } from "../Strategies/OAuth/DiscordOAuth.ts";
4 |
5 | describe('DiscordOAuth\'s sendDirect function should create the correct URI to redirect client to Discord', () => {
6 | const testClient = init({
7 | provider: 'Discord',
8 | client_id: 'test-client-id',
9 | client_secret: 'test-client-secret',
10 | scope: 'test-scope',
11 | redirect_uri: 'http://localhost:8080/OAuth/discord/token',
12 | }) as DiscordOAuth;
13 |
14 | const testURI = testClient.uriBuilder();
15 |
16 | assertEquals(
17 | testURI,
18 | 'https://discord.com/api/oauth2/authorize?client_id=test-client-id&redirect_uri=http://localhost:8080/OAuth/discord/token&scope=test-scope&response_type=code'
19 | );
20 | });
21 |
--------------------------------------------------------------------------------
/__tests__/FacebookOAuth-test.ts:
--------------------------------------------------------------------------------
1 | import { describe, assertEquals } from "../deps.ts";
2 | import { init } from "../mod.ts";
3 | import { FacebookOAuth } from "../Strategies/OAuth/FacebookOAuth.ts";
4 |
5 | describe('FacebookOAuth\'s sendDirect function should create the correct URI to redirect client to Facebook', () => {
6 | const testClient = init({
7 | provider: 'Facebook',
8 | client_id: 'test-client-id',
9 | client_secret: 'test-client-secret',
10 | scope: 'test-scope',
11 | redirect_uri: 'http://localhost:8080/oauth/facebook/token',
12 | }) as FacebookOAuth;
13 |
14 | const testURI = testClient.uriBuilder();
15 |
16 | assertEquals(
17 | testURI,
18 | 'https://www.facebook.com/v13.0/dialog/oauth?client_id=test-client-id&redirect_uri=http://localhost:8080/oauth/facebook/token&scope=test-scope&response_type=code'
19 | );
20 | });
21 |
--------------------------------------------------------------------------------
/__tests__/GithubOAuth-test.ts:
--------------------------------------------------------------------------------
1 | import { describe, assertEquals } from "../deps.ts";
2 | import { init } from "../mod.ts";
3 | import { GithubOAuth } from "../Strategies/OAuth/GithubOAuth.ts";
4 |
5 | describe('GithubOAut\'s sendDirect function should create the correct URI to redirect client to Github', () => {
6 | const testClient = init({
7 | provider: 'Github',
8 | client_id: 'test-client-id',
9 | client_secret: 'test-client-secret',
10 | scope: 'test-scope',
11 | redirect_uri: 'http://localhost:8080/OAuth/Github/token',
12 | }) as GithubOAuth;
13 |
14 | const testURI = testClient.uriBuilder();
15 |
16 | assertEquals(
17 | testURI,
18 | 'http://github.com/login/oauth/authorize?client_id=test-client-id&redirect_uri=http://localhost:8080/OAuth/Github/token&scope=test-scope&response_type=code',
19 | );
20 | });
21 |
--------------------------------------------------------------------------------
/__tests__/GoogleOAuth-test.ts:
--------------------------------------------------------------------------------
1 | import { describe, assertEquals } from "../deps.ts";
2 | import { init } from "../mod.ts";
3 | import { GoogleOAuth } from "../Strategies/OAuth/GoogleOAuth.ts";
4 |
5 | describe('GoogleOAuth\'s sendDirect function should create the correct URI to redirect client to Google', () => {
6 | const testClient = init({
7 | provider: 'Google',
8 | client_id: 'test-client-id',
9 | client_secret: 'test-client-secret',
10 | scope: 'test-scope',
11 | redirect_uri: 'http://localhost:8080/OAuth/Google/token'
12 | }) as GoogleOAuth;
13 |
14 | const testURI = testClient.uriBuilder();
15 |
16 | assertEquals(
17 | testURI,
18 | 'http://accounts.google.com/o/oauth2/v2/auth?client_id=test-client-id&redirect_uri=http://localhost:8080/OAuth/Google/token&scope=test-scope&response_type=code'
19 | );
20 | });
21 |
--------------------------------------------------------------------------------
/__tests__/LinkedinOAuth-test.ts:
--------------------------------------------------------------------------------
1 | import { describe, assertEquals } from "../deps.ts";
2 | import { init } from "../mod.ts";
3 | import { LinkedinOAuth } from "../Strategies/OAuth/LinkedinOAuth.ts";
4 |
5 | describe('LinkedinOAuth\'s sendDirect function should create the correct URI to redirect client to Linkedin', () => {
6 | const testClient = init({
7 | provider: 'Linkedin',
8 | client_id: 'test-client-id',
9 | client_secret: 'test-client-secret',
10 | scope: 'test-scope',
11 | redirect_uri: 'http://localhost:8080/OAuth/linkedin/token',
12 | }) as LinkedinOAuth;
13 |
14 | const testURI = testClient.uriBuilder();
15 |
16 | assertEquals(
17 | testURI,
18 | 'https://www.linkedin.com/oauth/v2/authorization?client_id=test-client-id&redirect_uri=http://localhost:8080/OAuth/linkedin/token&scope=test-scope&response_type=code',
19 | );
20 | });
21 |
--------------------------------------------------------------------------------
/__tests__/LocalAuth-test.ts:
--------------------------------------------------------------------------------
1 | import { generateTOTP } from "../Strategies/MFA/totp.ts";
2 | import { LocalAuth } from "../Strategies/MFA/LocalAuth.ts"
3 | import { describe, it, assertEquals, assertMatch } from "../deps.ts";
4 |
5 |
6 | describe('TOTP verification tests', () => {
7 | it('generateTOTP returns array of TOTP codes based off TOTP secret', async () => {
8 | const testSecret = 'GV7HO2JDO5SNTFLEPCCLKOANIN3VWLOH';
9 | const timeSteps = 55090869;
10 | const codes = await generateTOTP(testSecret, timeSteps);
11 | assertEquals(
12 | codes,
13 | [ "213967", "814450", "510712" ]
14 | )
15 | });
16 |
17 | it('generateTOTPSecret generates Base32 secret', () => {
18 | const secret = LocalAuth.generateTOTPSecret();
19 | const Base32RegExTest = /^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?$/;
20 | assertMatch(secret, Base32RegExTest);
21 | });
22 | })
--------------------------------------------------------------------------------
/__tests__/TwitterOAuth-test.ts:
--------------------------------------------------------------------------------
1 | import { describe, assertEquals } from "../deps.ts";
2 | import { init } from "../mod.ts";
3 | import { TwitterOAuth } from "../Strategies/OAuth/TwitterOAuth.ts";
4 |
5 | describe('TwitterOAuth\'s sendDirect function should create the correct URI to redirect client to Twitter', () => {
6 | const testClient = init({
7 | provider: 'Twitter',
8 | client_id: 'test-client-id',
9 | client_secret: 'test-client-secret',
10 | scope: 'test-scope',
11 | redirect_uri: 'http://127.0.0.1:8080/OAuth/twitter/token',
12 | }) as TwitterOAuth;
13 |
14 | const testURI = testClient.uriBuilder();
15 |
16 | assertEquals(
17 | testURI,
18 | 'https://twitter.com/i/oauth2/authorize?client_id=test-client-id&redirect_uri=http://127.0.0.1:8080/OAuth/twitter/token&scope=test-scope&response_type=code',
19 | );
20 | });
21 |
--------------------------------------------------------------------------------
/__tests__/bedrock-test.ts:
--------------------------------------------------------------------------------
1 | import { assertInstanceOf, Context, describe, it } from "../deps.ts";
2 | import { init } from "../mod.ts";
3 | import { GithubOAuth, FacebookOAuth, GoogleOAuth, LinkedinOAuth, DiscordOAuth, TwitterOAuth, LocalAuth } from "../strategies.ts";
4 |
5 | /**
6 | * Unit testing to ensure that the init function corrects the correct instance based on the provided parameters
7 | *
8 | * Note: testing to see if inputting wrong provider throws an error is not needed
9 | * this is because there is type checking of provider is done at instantiation via init function
10 | */
11 | describe('init should create correct instance of class', ()=> {
12 |
13 | it('github', ()=> {
14 | const testGithub = init({
15 | provider: "Github",
16 | client_id: "test-client-id",
17 | client_secret: "test-client-secret",
18 | scope: "read:user",
19 | redirect_uri: 'http://localhost:8080/OAuth/Github/token',
20 | });
21 |
22 | assertInstanceOf(testGithub, GithubOAuth);
23 | })
24 |
25 | it('facebook', ()=> {
26 | const testFacebook = init({
27 | provider: "Facebook",
28 | client_id: "test-client-id",
29 | client_secret: "test-client-secret",
30 | redirect_uri: 'http://localhost:8080/OAuth/facebook/token',
31 | scope: "public_profile",
32 | });
33 |
34 | assertInstanceOf(testFacebook, FacebookOAuth);
35 | })
36 |
37 | it('google', ()=> {
38 | const testGoogle = init({
39 | provider: "Google",
40 | client_id: "test-client-id",
41 | client_secret: "test-client-secret",
42 | scope: "openid",
43 | redirect_uri: 'http://localhost:8080/oauth/google/token',
44 | });
45 |
46 | assertInstanceOf(testGoogle, GoogleOAuth);
47 | })
48 |
49 | it('linkedin', ()=> {
50 | const testLinkedin = init({
51 | provider: "Linkedin",
52 | client_id: "test-client-id",
53 | client_secret: "test-client-secret",
54 | scope: "r_liteprofile",
55 | redirect_uri: 'http://localhost:8080/OAuth/linkedin/token',
56 | }) as LinkedinOAuth;
57 |
58 | assertInstanceOf(testLinkedin, LinkedinOAuth);
59 | })
60 |
61 | it('discord', ()=> {
62 | const testDiscord = init({
63 | provider: "Discord",
64 | client_id: "test-client-id",
65 | client_secret: "test-client-secret",
66 | redirect_uri: 'http://localhost:8080/OAuth/discord/token',
67 | scope: "identify",
68 | }) as DiscordOAuth;
69 |
70 | assertInstanceOf(testDiscord, DiscordOAuth);
71 | })
72 |
73 | it('twitter', ()=> {
74 | const testTwitter = init({
75 | provider: "Twitter",
76 | client_id: "test-client-id",
77 | client_secret: "test-client-secret",
78 | redirect_uri: 'http://127.0.0.1:8080/OAuth/twitter/token',
79 | scope: 'tweet.read users.read follows.read follows.write',
80 | });
81 |
82 | assertInstanceOf(testTwitter, TwitterOAuth);
83 | })
84 |
85 | it('local auth', ()=> {
86 | //a test function that fulfills the LocalAuth parameter type that requires a function called checkCreds
87 | //this will need to return a Promise
88 | const testFn = (): Promise => {
89 | return new Promise((resolve, reject) => {
90 | resolve(true);
91 | reject(false);
92 | })
93 | }
94 |
95 | const testLocal = init({
96 | provider: 'Local',
97 | readCreds: async (ctx: Context): Promise => {
98 | const body = await ctx.request.body();
99 | const bodyValue = await body.value;
100 | const { username, password } = bodyValue;
101 | return [username, password];
102 | },
103 | checkCreds: testFn
104 | })
105 |
106 | assertInstanceOf(testLocal, LocalAuth);
107 | })
108 | })
109 |
110 |
--------------------------------------------------------------------------------
/__tests__/twilio-test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals, assertInstanceOf, assertStringIncludes,assertMatch, assertExists, describe, it } from '../deps.ts'
2 | import { Twilio } from '../Strategies/MFA/twilio.ts'
3 | import {encode64} from '../deps.ts'
4 |
5 | describe("Testing creation of TwilioSMS class", () => {
6 | const testClass = new Twilio('testAccountSID', 'JDKSAJIWDJLIWJIQDJIDSADA4223DASD', 'testAuthToken');
7 |
8 | it("Testing to see if testClass is instance of TwilioSMS", ()=> {
9 | assertInstanceOf(testClass, Twilio);
10 | })
11 |
12 | it("Testing to see if authorization header is correct", () => {
13 | assertStringIncludes(testClass.authorizationHeader, 'Basic');
14 | })
15 |
16 | it('Testing to see if encoded portion of authorization header is correctly in Base64', () => {
17 | // testAccountSID:testAuthToken
18 | const encoded = encode64('testAccountSID:testAuthToken')
19 | const Base64RegEx = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
20 |
21 | assertMatch(encoded, Base64RegEx);
22 | })
23 |
24 | it("Testing to ensure authorization header includes base 64 following 'basic' ", () => {
25 | // testAccountSID:testAuthToken
26 | const encoded = encode64('testAccountSID:testAuthToken')
27 | const correctAuthHeader = 'Basic ' + encoded;
28 |
29 | assertEquals(testClass.authorizationHeader, correctAuthHeader);
30 |
31 | })
32 | })
33 |
34 | describe ("Testing TwilioSMS sendSMS function", () => {
35 | const testClass = new Twilio('testAccountSID', 'JDKSAJIWDJLIWJIQDJIDSADA4223DASD', 'testAuthToken');
36 | assertExists(testClass.sendSms);
37 | })
--------------------------------------------------------------------------------
/deps.ts:
--------------------------------------------------------------------------------
1 | export { helpers, Context } from "https://deno.land/x/oak@v10.5.1/mod.ts";
2 | export { decode as decode64, encode as encode64 } from "https://deno.land/std@0.137.0/encoding/base64.ts";
3 | export { assert, assertEquals, assertInstanceOf, assertStringIncludes, assertMatch, assertExists } from 'https://deno.land/std@0.138.0/testing/asserts.ts';
4 | export { crypto } from "https://deno.land/std@0.136.0/crypto/mod.ts";
5 | export { decode as decode32 } from "https://deno.land/std@0.136.0/encoding/base32.ts";
6 | export { decode as decode64url, encode as encode64url } from "https://deno.land/std@0.139.0/encoding/base64url.ts";
7 | export { describe, it } from "https://deno.land/std@0.139.0/testing/bdd.ts";
8 | export { SMTPClient } from 'https://deno.land/x/denomailer@1.0.1/mod.ts';
9 |
10 | export type { SendConfig } from "https://deno.land/x/denomailer@1.0.1/mod.ts";
--------------------------------------------------------------------------------
/mod.ts:
--------------------------------------------------------------------------------
1 | import { DiscordOAuth, FacebookOAuth, GithubOAuth, GoogleOAuth, LinkedinOAuth, LocalAuth, TwitterOAuth, OAuth } from "./strategies.ts";
2 | import { LocalAuthParams, OAuthParams } from "./types.ts";
3 |
4 | /**
5 | * Strategy and StrategyParams type is a collection of various strategies and their respective parameter objects grouped for cleanliness
6 | */
7 | type Strategy = DiscordOAuth | FacebookOAuth | GithubOAuth | GoogleOAuth | LinkedinOAuth | LocalAuth | TwitterOAuth;
8 | type StrategyParams = LocalAuthParams | OAuthParams;
9 |
10 | /**
11 | * Bedrock's init function accepts a Params object and instantiates the appropriate class
12 | * based off the provider defined within the Params object
13 | * @param OAuthParams or LocalAuthParams
14 | * @returns Strategy
15 | */
16 |
17 | /**
18 | *
19 | * @param params StrategyParams object constructed with initialization details of Strategy of choice
20 | * @returns {OAuth | LocalAuth} Returns class with exposed Middleware functions based off chosen strategy
21 | */
22 | export function init(params: LocalAuthParams): LocalAuth;
23 | export function init(params: OAuthParams): OAuth;
24 | export function init(params: StrategyParams): LocalAuth | OAuth {
25 | let strategy: Strategy;
26 |
27 | switch (params.provider) {
28 | case "Local":
29 | strategy = new LocalAuth(params);
30 | break;
31 | case "Discord":
32 | strategy = new DiscordOAuth(params);
33 | break;
34 | case "Google":
35 | strategy = new GoogleOAuth(params);
36 | break;
37 | case "Github":
38 | strategy = new GithubOAuth(params);
39 | break;
40 | case "Linkedin":
41 | strategy = new LinkedinOAuth(params);
42 | break;
43 | case "Facebook":
44 | strategy = new FacebookOAuth(params);
45 | break;
46 | case "Twitter":
47 | strategy = new TwitterOAuth(params);
48 | break;
49 | default:
50 | throw new Error(
51 | "Invalid input on init constuctor - see log for more information",
52 | );
53 | }
54 |
55 | // will provide developer a log that will inform which strategy has been initialized
56 | console.info(`Successfully initialized ${params.provider} strategy!`);
57 | return strategy;
58 | }
59 |
--------------------------------------------------------------------------------
/strategies.ts:
--------------------------------------------------------------------------------
1 | export { LocalAuth } from "./Strategies/MFA/LocalAuth.ts";
2 | export { GoogleOAuth } from "./Strategies/OAuth/GoogleOAuth.ts";
3 | export { GithubOAuth } from "./Strategies/OAuth/GithubOAuth.ts";
4 | export { LinkedinOAuth } from "./Strategies/OAuth/LinkedinOAuth.ts";
5 | export { DiscordOAuth } from "./Strategies/OAuth/DiscordOAuth.ts";
6 | export { FacebookOAuth } from "./Strategies/OAuth/FacebookOAuth.ts";
7 | export { TwitterOAuth } from "./Strategies/OAuth/TwitterOAuth.ts";
8 | export { Auth } from "./Strategies/Auth.ts";
9 | export { OAuth } from "./Strategies/OAuth/OAuth.ts";
--------------------------------------------------------------------------------
/types.ts:
--------------------------------------------------------------------------------
1 | import { Context } from "./deps.ts";
2 |
3 | /**
4 | * Defined below are the types to structure arguments across various classes
5 | * and methods in the Bedrock library
6 | */
7 |
8 | /**
9 | * LocalAuthParams type defines the parameters required to instantiate an
10 | * instance of the LocalAuth class. Aliases are used to account for varying
11 | * parameters depending on MFA type.
12 | */
13 | export type LocalAuthParams = {
14 | provider: "Local";
15 | mfaType: "Token";
16 | checkCreds: (username: string, password: string) => Promise;
17 | getSecret: (username: string) => Promise;
18 | readCreds?: (ctx: Context) => Promise;
19 | } | {
20 | provider: "Local";
21 | mfaType: "SMS";
22 | checkCreds: (username: string, password: string) => Promise;
23 | getSecret: (username: string) => Promise;
24 | getNumber: (username: string) => Promise;
25 | sourceNumber: string;
26 | accountSID: string;
27 | authToken: string;
28 | readCreds?: (ctx: Context) => Promise;
29 | } | {
30 | provider: "Local";
31 | checkCreds: (username: string, password: string) => Promise;
32 | readCreds?: (ctx: Context) => Promise;
33 | } | {
34 | provider: "Local";
35 | mfaType: "Email";
36 | checkCreds: (username: string, password: string) => Promise;
37 | getSecret: (username: string) => Promise;
38 | clientOptions: ClientOptions;
39 | fromAddress: string;
40 | getEmail: (username: string) => Promise;
41 | readCreds?: (ctx: Context) => Promise;
42 | };
43 |
44 | /**
45 | * SMSRequest type is defined to structure postSMSRequest function's parameter in Twilio class
46 | * Structured to ensure proper inputs that are needed by the Twilio SMS API
47 | */
48 | export type SMSRequest = {
49 | From: string; //the Twilio phone number to used to send an SMS
50 | To: string; //phone number to receive SMS
51 | Body: string; //SMS content
52 | }
53 |
54 | /**
55 | * Incoming type is defined to structure the sendSMS function's parameter in Twilio class
56 | * Structured to ensure proper inputs that represent from and to
57 | */
58 | export type Incoming = {
59 | From: string; //the Twilio phone number used to send an SMS
60 | To: string; //phone number to receive SMS
61 | }
62 |
63 | /**
64 | * ClientOptions type is defined to structure parameters required to instantiate
65 | * an instance of DenoMailer. It accepts parameters required to setup a connection
66 | * with a SMTP server of the developer's choice.
67 | */
68 | export type ClientOptions = {
69 | connection: {
70 | hostname: string;
71 | port?: number;
72 | tls?: boolean;
73 | auth?: {
74 | username: string;
75 | password: string;
76 | };
77 | };
78 | }
79 |
80 | /**
81 | * OAuthParams type defines the parameters required across all OAuth providers
82 | * and are used to instantiate an instance of the OAuth subclass base off the
83 | * provider. Google has a defined alias due to optional parameters available to
84 | * it which we are restricting from other OAuth providers
85 | */
86 | export type OAuthParams = {
87 | provider: 'Github' | 'Facebook' | 'Twitter' | 'Linkedin' | 'Discord';
88 | client_id: string;
89 | client_secret: string;
90 | redirect_uri: string;
91 | scope: string;
92 | } | {
93 | provider: 'Google';
94 | client_id: string;
95 | client_secret: string;
96 | redirect_uri: string;
97 | scope: string;
98 | access_type?: "online" | "offline";
99 | prompt?: "none" | "consent" | "select_account";
100 | }
--------------------------------------------------------------------------------