├── .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 |

drawing

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 | } --------------------------------------------------------------------------------