├── gitHub.test.ts ├── mod.ts ├── README.md ├── LICENSE ├── types.ts └── githubStrategy.ts /gitHub.test.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import GithubStrategy from './githubStrategy.ts'; 2 | export default GithubStrategy; 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dashport-githubstrategy 2 | Github OAuth 2.0 strategy for [Dashport](https://github.com/oslabs-beta/dashport) module for Deno 3 | ``` 4 | import GitHubStrategy from 'https://deno.land/x/dashport_github/mod.ts' 5 | ``` 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Should contain the same properties and methods defined by Oak 3 | * https://github.com/oakserver/oak 4 | */ 5 | export interface OakContext { 6 | app: any; 7 | cookies: any; 8 | request: any; 9 | respond: any; 10 | response: any; 11 | socket: any; 12 | state: any; 13 | assert: Function; 14 | send: Function; 15 | sendEvents: Function; 16 | throw: Function; 17 | upgrade: Function; 18 | params: any; 19 | locals?: any; 20 | } 21 | 22 | /** 23 | * Different OAuths will return different user information in different 24 | * structures. Dashport strategies should break down and reconstruct the user 25 | * info into the standardized UserProfile below 26 | */ 27 | export interface UserProfile { 28 | // the provider the user is authenticated with 29 | provider: string; 30 | // the unique id a user has with that specific provider 31 | providerUserId: string; 32 | // the display name or username for this specific user 33 | displayName?: string; 34 | name?: { 35 | familyName?: string; 36 | givenName?: string; 37 | middleName?: string; 38 | }; 39 | emails?: Array; 40 | } 41 | 42 | /** 43 | * At the bare minimum, OAuth 2.0 providers will require a client ID, client 44 | * secret, and redirect URI. The remaining options depend on the OAuth 2.0 45 | * provider, such as scope 46 | */ 47 | export interface Options { 48 | client_id: string; 49 | client_secret: string; 50 | redirect_uri: string; 51 | [option: string]: string; 52 | } 53 | 54 | /** 55 | * All OAuth 2.0 providers will provide access tokens 56 | */ 57 | export interface TokenData { 58 | access_token: string; 59 | expires_in?: number; 60 | scope?: string; 61 | token_type?: string; 62 | id_token?: string; 63 | refresh_token?: string; 64 | } 65 | 66 | /** 67 | * The form the information from strategies should come back in 68 | */ 69 | export interface AuthData { 70 | tokenData: TokenData; 71 | userInfo: UserProfile; 72 | } 73 | -------------------------------------------------------------------------------- /githubStrategy.ts: -------------------------------------------------------------------------------- 1 | import { OakContext, Options, AuthData, TokenData } from './types.ts'; 2 | 3 | /** 4 | * Create an instance of 'GitHubStrategy'. 5 | * 6 | * Options: 7 | * 8 | * - client_id: string Required 9 | * - redirect_uri: string Required 10 | * - client_secret: string Required 11 | * 12 | * reference: https://docs.github.com/en/developers/apps/authorizing-oauth-apps 13 | */ 14 | export default class GitHubStrategy { 15 | name: string = 'github' 16 | options: Options; 17 | uriFromParams: string; 18 | authURL: string = 'https://github.com/login/oauth/authorize?'; 19 | tokenURL: string = 'https://github.com/login/oauth/access_token?'; 20 | authDataURL: string = 'https://api.github.com/user?'; 21 | /** 22 | * @constructor 23 | * @param {Object} options 24 | * @api public 25 | */ 26 | constructor (options: Options) { 27 | // customize required/desired field for the first redirect 28 | if (!options.client_id || !options.redirect_uri || !options.client_secret) { 29 | throw new Error('ERROR in GitHubStrategy constructor: Missing required arguments'); 30 | } 31 | 32 | this.options = options; 33 | 34 | // PRE STEP 1: 35 | // Constructs the second half of the authURL for developer's first endpoint from the info put into 'options' 36 | this.uriFromParams = this.constructURI(this.options); 37 | } 38 | 39 | constructURI(options: Options, skip?: string[]): any { 40 | const paramArray: string[][] = Object.entries(options); 41 | let paramString: string = ''; 42 | 43 | for (let i = 0; i < paramArray.length; i++) { 44 | let [key, value] = paramArray[i]; 45 | 46 | if (skip && skip.includes(key)) continue; 47 | // adds the key and '=' for every member of options not in the skip array 48 | paramString += (key + '='); 49 | // adds the value and '&' for every member of options not in the skip array 50 | paramString += (value + '&'); 51 | } 52 | 53 | // removes the '&' that was just placed at the end of the string 54 | if (paramString[paramString.length - 1] === '&') { 55 | paramString = paramString.slice(0, -1); 56 | } 57 | 58 | return paramString; 59 | } 60 | 61 | // parses an encoded URI 62 | parseCode(encodedCode: string): string { 63 | const decodedString: { [name: string] : string } = { 64 | "%24": "$", 65 | "%26": "&", 66 | "%2B": "+", 67 | "%2C": ",", 68 | "%2F": "/", 69 | "%3A": ":", 70 | "%3B": ";", 71 | "%3D": "=", 72 | "%3F": "?", 73 | "%40": "@" 74 | } 75 | const toReplaceArray: string[] = Object.keys(decodedString); 76 | 77 | for (let i = 0; i < toReplaceArray.length; i++) { 78 | while (encodedCode.includes(toReplaceArray[i])) { 79 | encodedCode = encodedCode.replace(toReplaceArray[i], decodedString[toReplaceArray[i]]); 80 | } 81 | } 82 | 83 | return encodedCode; 84 | } 85 | 86 | // ENTRY POINT 87 | async router(ctx: OakContext, next: Function) { 88 | // GO_Step 1 Request Permission 89 | if (!ctx.request.url.search) return await this.authorize(ctx, next); 90 | // GO_Step 3 Exchange code for Token 91 | if (ctx.request.url.search.slice(1, 5) === 'code') return this.getAuthToken(ctx, next) 92 | }; 93 | 94 | // STEP 1: sends the programatically constructed uri to an oauth 2.0 server 95 | async authorize(ctx: OakContext, next: Function) { 96 | return await ctx.response.redirect(this.authURL + this.uriFromParams); 97 | } 98 | 99 | // STEP 2: client choose yes or no 100 | 101 | // STEP 3: handle oauth 2.0 server response containing auth code 102 | // STEP 3.5: request access token in exchange for auth code 103 | async getAuthToken(ctx: OakContext, next: Function) { 104 | // the URI send back to the endpoint provided in step 1 105 | const OGURI: string = ctx.request.url.search; 106 | 107 | if (OGURI.includes('error')) { 108 | return new Error('ERROR in getAuthToken: Received an error from auth token code request.'); 109 | } 110 | 111 | // splits the string at the '=,' storing the first part in URI1[0] and the part we want in URI1[1] 112 | let URI1: string[] = OGURI.split('='); 113 | // splits the string at the ampersand(&), storing the string with the access_token in URI2[0] 114 | // and the other parameters at URI2[n] 115 | const URI2: string[] = URI1[1].split('&'); 116 | // PARSE THE URI 117 | const code: string = this.parseCode(URI2[0]); 118 | 119 | // STEP 3.5 120 | const tokenOptions: Options = { 121 | client_id: this.options.client_id, 122 | client_secret: this.options.client_secret, 123 | redirect_uri: this.options.redirect_uri, 124 | code: code, 125 | } 126 | 127 | // SEND A FETCH REQ FOR TOKEN 128 | try { 129 | const options: any = { 130 | method: 'POST', 131 | headers: { 132 | 'Accept': 'application/json', 133 | 'Content-Type': 'application/json' 134 | }, 135 | body: JSON.stringify(tokenOptions) 136 | }; 137 | 138 | let data: any = await fetch('https://github.com/login/oauth/access_token', options); 139 | data = await data.json(); 140 | 141 | if (data.type === 'oAuthException') { 142 | return new Error('ERROR in getAuthToken: Token request threw OAuth exception.'); 143 | } 144 | 145 | // PASSES TOKEN ON TO STEP 4 146 | return this.getAuthData(data); 147 | } catch(err) { 148 | return new Error(`ERROR in getAuthToken: Unable to obtain token - ${err}`); 149 | } 150 | } 151 | 152 | // STEP 4 get the access token from the returned data 153 | // STEP 4.5 exchange access token for user info 154 | async getAuthData(parsed: TokenData){ 155 | const authData: AuthData = { 156 | tokenData: { 157 | access_token: parsed.access_token, 158 | scope: parsed.scope, 159 | token_type: parsed.token_type, 160 | }, 161 | userInfo: { 162 | provider: '', 163 | providerUserId: '' 164 | } 165 | } 166 | 167 | // STEP 4.5: request user info 168 | try { 169 | const authOptions: any = { 170 | headers: { 171 | 'Authorization': `token ${authData.tokenData.access_token}` 172 | }, 173 | }; 174 | let data: any = await fetch('https://api.github.com/user', authOptions); 175 | data = await data.json(); 176 | 177 | authData.userInfo = { 178 | provider: this.name, 179 | providerUserId: data.id, 180 | displayName: data.login, 181 | name: { 182 | familyName: data.name, 183 | }, 184 | emails: [data.email], 185 | }; 186 | 187 | return authData; 188 | } catch(err) { 189 | return new Error(`ERROR in getAuthData: Unable to obtain auth data - ${err}`); 190 | } 191 | } 192 | } 193 | --------------------------------------------------------------------------------