├── .gitignore ├── LICENSE ├── README.md ├── __tests__ └── dashportOak.test.js ├── dashports ├── dashportOak.ts └── templateDashport.ts ├── deprecated └── v1.0.1 │ ├── dashport.ts │ ├── sessionManager.test.ts │ └── sessionManager.ts ├── deps.ts ├── mod.ts ├── templateStrategy.ts └── types.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Dashport logo 3 |

4 | 5 | *

Local authentication and OAuth 2.0 middleware for Deno

* 6 | 7 |

8 |
9 | 10 | License 11 | 12 | 13 | Open issues 14 | 15 | 16 | Last commit 17 | 18 | 19 | GitHub stars 20 | 21 |

22 | 23 | # Features 24 | - Dashport classes that handle authentication and sessions based on the server framework (currently only Oak supported). 25 | - A [local strategy](https://github.com/oslabs-beta/dashport-localstrategy) module. 26 | - Strategy modules that allow developers to use third-party OAuth 2.0 27 | - [x] [Google](https://github.com/oslabs-beta/dashport-googlestrategy) 28 | - [x] [Facebook](https://github.com/oslabs-beta/dashport-facebookstrategy) 29 | - [x] [Github](https://github.com/oslabs-beta/dashport-githubstrategy) 30 | - [x] [LinkedIn](https://github.com/oslabs-beta/dashport-linkedinstrategy) 31 | - [x] [Spotify](https://github.com/oslabs-beta/dashport-spotifystrategy) 32 | - Written in TypeScript. 33 | 34 | # Updates 35 | 36 | ## v1.1.0 to v1.2.0 37 | - Require Application instance for Oak to be passed in when instantiating DashportOak, removing the need for developers to manually call app.use(dashport.initialize). 38 | 39 | ## v1.0.1 to v1.1.0 40 | - Instead of passing in the name of the server framework being used when Dashport is instantiated, Dashport now has different classes for different server frameworks. This is to support better modularity. 41 | - Added a Dashport class for Oak, DashportOak. 42 | - A template Dashport has been provided for any developer to create their own Dashport for a server framework. 43 | - Refactored authenticate to take three arguments instead of one - the strategy, the serializer, and the deserializer to be used vs the name of the strategy. 44 | - Removed add/remove serializer/deserializer methods. 45 | - Merged deserializer's functionality into authenticate. 46 | 47 | # Overview 48 | Dashport is a module that simplifies authentication in Deno. Currently, there is only Dashport support for the server framework Oak, but there is a template Dashport class available for anyone to create their own compatible Dashport. 49 | 50 | Dashport was inspired by [Passport](http://www.passportjs.org/), the golden standard of authentication middleware for Node.js. 51 | 52 | # Getting Started 53 | Below is an example using the Oak server framework. To get started, import Dashport into the server file. 54 | 55 | ```typescript 56 | // 'server.ts' file 57 | import { DashportOak } from 'https://deno.land/x/dashport@v1.2.1/mod.ts'; 58 | ``` 59 | 60 | In the future, additional Dashports can be imported from the same mod.ts file. For example if Express was supported in Deno: 61 | 62 | ```typescript 63 | import { DashportExpress } from 'https://deno.land/x/dashport@v1.2.1/mod.ts'; 64 | ``` 65 | 66 | After importing Dashport, import Application from Oak and instantiate it. 67 | 68 | ```typescript 69 | // 'server.ts' file 70 | import { DashportOak } from 'https://deno.land/x/dashport@v1.2.1/mod.ts'; 71 | import { Application } from "https://deno.land/x/oak/mod.ts"; 72 | 73 | const app = new Application(); 74 | ``` 75 | 76 | Then instantiate Dashport and pass in the Application instance. This is needed in order to add a Dashport property onto Oak's context object. This specific instance of Dashport will then need to be used when calling the methods authenticate and logOut, so sessions for users can be maintained. 77 | 78 | ```typescript 79 | // 'server.ts' file 80 | import { DashportOak } from 'https://deno.land/x/dashport@v1.2.1/mod.ts'; 81 | import { Application } from "https://deno.land/x/oak/mod.ts"; 82 | 83 | const app = new Application(); 84 | const dashport = new DashportOak(app); 85 | ``` 86 | 87 | Routes can then be added as needed 88 | 89 | ```typescript 90 | import { DashportOak } from 'https://deno.land/x/dashport@v1.2.1/mod.ts'; 91 | import { Application, Router } from 'https://deno.land/x/oak/mod.ts'; 92 | 93 | const app = new Application(); 94 | const router = new Router(); 95 | 96 | const dashport = new DashportOak(app); 97 | 98 | // add routes here 99 | 100 | app.use(router.routes()); 101 | app.use(router.allowedMethods()); 102 | 103 | app.addEventListener("error", (evt) => { 104 | console.log(evt.error); 105 | }); 106 | 107 | console.log('running on port', port); 108 | await app.listen({ port }); 109 | ``` 110 | 111 | In a separate file, add Dashport configurations for a [strategy](#strategies), [serializer](#serializers), and [deserializer](#deserializers). Developers can configure as many strategies, serializers, and deserializers, as they want, as long as they follow the specific rules for each one. 112 | 113 | ```typescript 114 | // 'dashportConfig.ts' file 115 | import GoogleStrategy from 'https://deno.land/x/dashport_google/mod.ts'; 116 | import GitHubStrategy from 'https://deno.land/x/dashport_github/mod.ts' 117 | 118 | export const googStrat = new GoogleStrategy({ 119 | client_id: 'client-id-here', 120 | client_secret: 'client-secret-here', 121 | redirect_uri: 'http://localhost:8000/privatepage', 122 | response_type: 'code', 123 | scope: 'profile email openid', 124 | grant_type: 'authorization_code', 125 | }); 126 | 127 | export const ghStrat = new GitHubStrategy({ 128 | client_id: 'client-id-here', 129 | client_secret: 'client-secret-here', 130 | redirect_uri: 'http://localhost:8000/privatepage', 131 | }) 132 | 133 | export const serializerA = async (userInfo: any) => { 134 | const serializedId = Math.floor(Math.random() * 1000000000); 135 | userInfo.id = serializedId; 136 | 137 | try { 138 | await exampleDbCreateUpsert(userInfo); 139 | return serializedId; 140 | } catch(err) { 141 | return err; 142 | // or return new Error(err); 143 | } 144 | }; 145 | 146 | export const serializerB = async (userInfo: any) => { 147 | ... 148 | } 149 | 150 | export const deserializerA = async (serializedId: (string | number)) => { 151 | try { 152 | const userInfo = await exampleDbFind({ id: serializedId }); 153 | return userInfo; 154 | } catch(err) { 155 | return err; 156 | // or return new Error(err); 157 | } 158 | }; 159 | 160 | export const deserializerB = async (serializedId: (string | number)) => { 161 | ... 162 | } 163 | ``` 164 | 165 | Import these configurations into the server.ts file and Dashport is now ready to authenticate. Dashport's authenticate method acts as middleware, so it can be used like so: 166 | 167 | ```typescript 168 | import { googStrat, serializerA, deserializerA } from './dashportConfig.ts'; 169 | 170 | router.get('/privatepage', 171 | dashport.authenticate(googStrat, serializerA, deserializerA), 172 | async (ctx: any, next: any) => { 173 | ctx.response.body = 'This is a private page!'; 174 | } 175 | ) 176 | ``` 177 | 178 | After authentication, Dashport will have created an ID based on the developer's defined serializer and manipulated the data however the developer specified (such as storing user info in a database). The ID then gets stored by Dashport to create a persistent session. Dashport will then invoke the developer's defined deserializer and pass in the ID. The deserializer will take this ID and manipulate it however the developer specified (such as fetching user info from the database). The returned data from the deserializer then gets stored on **ctx.locals** so the next middleware can access it. If an error occurred in the deserializer function and gets returned, the error will be stored on **ctx.locals**. 179 | 180 | ```typescript 181 | router.get('/user-favorites', 182 | dashport.authenticate(googStrat, serializerA, deserializerA), 183 | async (ctx: any, next: any) => { 184 | if (ctx.locals instanceof Error) { 185 | ctx.response.body = 'An Error occurred!'; 186 | } else { 187 | const displayName = ctx.locals.displayName; 188 | ctx.response.body = `Welcome ${displayName}!`; 189 | } 190 | } 191 | ) 192 | ``` 193 | 194 | In order to end a session, a log out button can be routed to an endpoint that calls Dashport's logOut method. Here's an example use with Oak: 195 | 196 | ```typescript 197 | router.get('/log-out', 198 | dashport.logOut, 199 | async (ctx: any, next: any) => { 200 | ctx.response.body = "You've logged out"; 201 | } 202 | ) 203 | ``` 204 | 205 | # Strategies 206 | - Strategies are the modules that can be imported with Dashport to allow third-party OAuth. They specify the authentication logic for a given OAuth service provider. 207 | - Strategy classes need to follow two rules: 208 | 1. Have a router method that ultimately returns an Error or the user information returned by the third-party OAuth in the form of Dashport's defined [AuthData](#authdata) interface. AuthData needs to have a **userInfo** property in the form of [UserProfile](#userprofile) and a **tokenData** property in the form of [TokenData](#tokendata). 209 | 2. When instantiated, take in the options that are needed for the specific third-party OAuth to authenticate. 210 | 211 | ```typescript 212 | const googStrat = new GoogleStrategy({ 213 | client_id: 'client-id-here', 214 | client_secret: 'client-secret-here', 215 | redirect_uri: 'http://localhost:8000/privatepage', 216 | response_type: 'code', 217 | scope: 'profile email openid', 218 | grant_type: 'authorization_code', 219 | }); 220 | ``` 221 | 222 | # Serializers 223 | - After a successful authentication, Dashport will pass the obtained user information to a serializer. It is up to the developer to define serializer functions that specify what to do with user information. User information will be passed in the form of Dashport's defined [AuthData](#authdata) interface. 224 | - Serializer functions need to follow four rules: 225 | 1. Accept one argument: the user data in the form of an object. 226 | 2. Specify how to create a serialized ID. 227 | 3. Specify what the developer wants to do with the user data (e.g. store it in a database). 228 | 4. Return the serialized ID or an Error. 229 | 230 | ```typescript 231 | const serializerA = async (userInfo: any) => { 232 | const serializedId = Math.floor(Math.random() * 1000000000); 233 | userInfo.id = serializedId; 234 | 235 | try { 236 | await exampleDbCreateUpsert(userInfo); 237 | return serializedId; 238 | } catch(err) { 239 | return err; 240 | // or return new Error(err); 241 | } 242 | }; 243 | ``` 244 | 245 | # Deserializers 246 | - After a successful authentication, Dashport will pass the serialized ID generated from the serializer to the deserializer. The developer should specify what the deserializer should do with the ID and return any data that should be available for access in the next middleware. The shape of the return value is up to the developer. In Oak, if the deserialization is successful, the data returned is stored on **ctx.locals**. If the deserialization is not successful, an Error will be stored on **ctx.locals**. 247 | - Deserializer functions need to follow three rules: 248 | 1. Accept one argument: the serialized ID. 249 | 2. Specify what the developer wants to do with the serialized ID (e.g. fetch user info from a database). 250 | 3. Return the data (e.g. user info) or an Error. 251 | 252 | ```typescript 253 | const deserializerA = async (serializedId: (string | number)) => { 254 | try { 255 | const userInfo = await exampleDbFind({ id: serializedId }); 256 | return userInfo; 257 | } catch(err) { 258 | return err; 259 | // or return new Error(err); 260 | } 261 | }; 262 | ``` 263 | 264 | # Dashport Methods 265 | 266 | ## authenticate 267 | - Exact functionality depends on which Dashport class is being used 268 | - Authenticate is the middleware function that powers Dashport. It takes in three arguments: the instantiation of a strategy to be used, a serializer function, and a deserializer function. Authenticate first checks if a session exists. If a session does not exist, it begins the authentication process for the specified strategy. 269 | 270 | ```typescript 271 | // Oak example 272 | import { DashportOak } from 'https://deno.land/x/dashport@v1.2.1/mod.ts'; 273 | import { Application, Router } from 'https://deno.land/x/oak/mod.ts'; 274 | import { ghStrat, serializerB, deserializerB } from './dashportConfig.ts'; 275 | 276 | const app = new Application(); 277 | const router = new Router(); 278 | 279 | const dashport = new DashportOak(app); 280 | 281 | app.use(router.routes()); 282 | app.use(router.allowedMethods()); 283 | 284 | router.get('/secretpage', 285 | dashport.authenticate(ghStrat, serializerB, deserializerB), 286 | async (ctx: any, next: any) => { 287 | ctx.response.body = 'This is a secret page!'; 288 | } 289 | ) 290 | ``` 291 | 292 | ## logOut 293 | - Exact functionality depends on which Dashport class is being used 294 | - logOut is a middleware function that ends a session. If a user logs out and logOut is used, the user will have to reauthenticate. Here is an example use with Oak: 295 | 296 | ```typescript 297 | router.get('/log-out', 298 | dashport.logOut, 299 | async (ctx: any, next: any) => { 300 | ctx.response.body = "You've logged out"; 301 | } 302 | ) 303 | ``` 304 | 305 | # Interfaces 306 | 307 | ## AuthData 308 | When a strategy successfully authenticates a user, the information given by the third-party provider should be returned in the form AuthData. The object should have an optional [tokenData](##tokendata) property and a required userInfo property in the form of [UserProfile](##userprofile). This contains the information for the [authenticate](##authenticate) method to use. The interface for AuthData is as below: 309 | 310 | ```typescript 311 | interface AuthData { 312 | tokenData: TokenData; 313 | userInfo: UserProfile; 314 | } 315 | ``` 316 | 317 | ## TokenData 318 | Any relevant token data the developer wishes to receive from the third-party OAuth should be stored in an object with the below interface: 319 | 320 | ```typescript 321 | interface TokenData { 322 | access_token: string; 323 | expires_in: number; 324 | scope: string; 325 | token_type: string; 326 | id_token: string; 327 | } 328 | ``` 329 | 330 | ## UserProfile 331 | Since every OAuth provider returns information in different names and shapes, it is up to each strategy to conform the data returned into Dashport's defined UserProfile interface. This should contain all data the developer wishes to use. 332 | 333 | ```typescript 334 | export interface UserProfile { 335 | provider: string; 336 | providerUserId: string; 337 | displayName?: string; 338 | name?: { 339 | familyName?: string; 340 | givenName?: string; 341 | middleName?: string; 342 | }; 343 | emails?: Array; 344 | } 345 | ``` 346 | 347 | # Stretch Features 348 | - Add more strategies. 349 | - Add support for other server frameworks. 350 | 351 | # How To Contribute 352 | We would love to hear your experience and get your feedback on our modules. Feel free to send us any issues, concerns, or suggestions, in our Issues section, or simply contact us through LinkedIn. 353 | 354 | # Developers 355 | 356 | [*Dashport website*](https://www.dashport.org/) 357 | 358 | Alex Nance :: [LinkedIn](https://www.linkedin.com/in/balexandernance/) | [GitHub](https://github.com/BAlexanderNance) 359 | 360 | Alvin Cheng :: [LinkedIn](https://www.linkedin.com/in/alvin-cheng/) | [GitHub](https://github.com/alcheng005) 361 | 362 | Edward Deng :: [LinkedIn](https://www.linkedin.com/in/edwarddeng-/) | [GitHub](https://github.com/ed-deng) 363 | 364 | Sam Portelance :: [LinkedIn](https://www.linkedin.com/in/sportelance/) | [GitHub](https://github.com/sportelance) 365 | 366 | Wei Huang :: [LinkedIn](https://www.linkedin.com/in/wei-waye-huang/) | [GitHub](https://github.com/waye-huang) 367 | 368 | # License 369 | This project is licensed under the [MIT License](https://github.com/oslabs-beta/DraQLa/blob/main/LICENSE) 370 | 371 | -------------------------------------------------------------------------------- /__tests__/dashportOak.test.js: -------------------------------------------------------------------------------- 1 | import { Application, assertEquals, assertThrows, assertThrowsAsync } from "../deps.ts"; 2 | import DashportOak from '../dashports/dashportOak.ts'; 3 | 4 | // Mock class and functions 5 | class TestStrat { 6 | async router(ctx, next) { 7 | if (ctx.state.testing = 'user') { 8 | return { 9 | authData: { 10 | access_token: 'AccessToken000' 11 | }, 12 | userInfo: { 13 | provider: 'test', 14 | providerUserId: 'ID12345' 15 | } 16 | } 17 | } 18 | 19 | return new Error('ERROR: This is an Error'); 20 | } 21 | } 22 | 23 | function fakeSerializer(userInfo) { 24 | return 'ABCDEF'; 25 | } 26 | 27 | function fakeDeserializer(serializedId) { 28 | return { 29 | firstName: 'Hello', 30 | lastName: 'World' 31 | }; 32 | } 33 | 34 | const fakeNext = () => 'fakeNext was invoked'; 35 | 36 | 37 | 38 | // INITIALIZE TEST 39 | Deno.test({ 40 | name: "initialize should throw if ctx.state is undefined.", 41 | fn() { 42 | const app = new Application(); 43 | const fakeOakCtx = { 44 | app: {}, 45 | cookies: {}, 46 | request: {}, 47 | respond: {}, 48 | response: {}, 49 | socket: {}, 50 | assert: () => 1, 51 | send: () => 2, 52 | sendEvents: () => 3, 53 | throw: () => 4, 54 | upgrade: () => 5, 55 | params: {} 56 | } 57 | 58 | const testDpOak = new DashportOak(app); 59 | assertThrowsAsync(async () => await testDpOak.initialize(fakeOakCtx, fakeNext)); 60 | }, 61 | }); 62 | 63 | Deno.test({ 64 | name: "initialize should add a _dashport object onto ctx.state.", 65 | async fn() { 66 | const app = new Application(); 67 | const fakeOakCtx = { 68 | app: {}, 69 | cookies: {}, 70 | request: {}, 71 | respond: {}, 72 | response: {}, 73 | socket: {}, 74 | assert: () => 1, 75 | send: () => 2, 76 | sendEvents: () => 3, 77 | throw: () => 4, 78 | upgrade: () => 5, 79 | params: {}, 80 | state: {} 81 | } 82 | 83 | const testDpOak = new DashportOak(app); 84 | await testDpOak.initialize(fakeOakCtx, fakeNext); 85 | 86 | assertEquals(Object.keys(fakeOakCtx.state)[0], '_dashport'); 87 | assertEquals(Object.values(fakeOakCtx.state)[0], {}); 88 | }, 89 | }); 90 | 91 | 92 | 93 | // AUTHENTICATE TESTS 94 | Deno.test({ 95 | name: "authenticate should throw an error if three arguments are not passed.", 96 | fn() { 97 | const app = new Application(); 98 | const testDpOak = new DashportOak(app); 99 | 100 | assertThrows(() => testDpOak.authenticate({test: 'foobar'})); 101 | assertThrows(() => testDpOak.authenticate({test: 'foobar'}, fakeSerializer)); 102 | assertThrows(() => testDpOak.authenticate(fakeSerializer, fakeDeserializer)); 103 | assertThrows(() => testDpOak.authenticate(fakeDeserializer)); 104 | }, 105 | }); 106 | 107 | 108 | // Strategy tests 109 | Deno.test({ 110 | name: "authenticate should throw an error if strategy is not an object.", 111 | fn() { 112 | const app = new Application(); 113 | const testDpOak = new DashportOak(app); 114 | 115 | assertThrows(() => testDpOak.authenticate('hello world', fakeSerializer, fakeDeserializer)); 116 | }, 117 | }); 118 | 119 | Deno.test({ 120 | name: "authenticate should throw an error if strategy does not have a router property.", 121 | fn() { 122 | const app = new Application(); 123 | const testDpOak = new DashportOak(app); 124 | 125 | assertThrows(() => testDpOak.authenticate({ test: 'hello world' }, fakeSerializer, fakeDeserializer)); 126 | }, 127 | }); 128 | 129 | Deno.test({ 130 | name: "authenticate should throw an error if strategy's router property is not a function.", 131 | fn() { 132 | const app = new Application(); 133 | const testDpOak = new DashportOak(app); 134 | 135 | assertThrows(() => testDpOak.authenticate({ router: 'hello world' }, fakeSerializer, fakeDeserializer)); 136 | }, 137 | }); 138 | 139 | 140 | // Serializer tests 141 | Deno.test({ 142 | name: "authenticate should throw an error if serializer is not a function.", 143 | fn() { 144 | const app = new Application(); 145 | const testDpOak = new DashportOak(app); 146 | 147 | assertThrows(() => testDpOak.authenticate(new TestStrat(), 'dashport', fakeDeserializer)); 148 | }, 149 | }); 150 | 151 | Deno.test({ 152 | name: "authenticate should throw an error if serializer does not take exactly 1 parameter.", 153 | fn() { 154 | const app = new Application(); 155 | const testDpOak = new DashportOak(app); 156 | 157 | assertThrows(() => testDpOak.authenticate(new TestStrat(), () => 1, fakeDeserializer)); 158 | assertThrows(() => testDpOak.authenticate(new TestStrat(), (test, testing) => 1, fakeDeserializer)); 159 | }, 160 | }); 161 | 162 | 163 | // Deserializer tests 164 | Deno.test({ 165 | name: "authenticate should throw an error if deserializer is not a function.", 166 | fn() { 167 | const app = new Application(); 168 | const testDpOak = new DashportOak(app); 169 | 170 | assertThrows(() => testDpOak.authenticate(new TestStrat(), fakeSerializer, 'dashport')); 171 | }, 172 | }); 173 | 174 | Deno.test({ 175 | name: "authenticate should throw an error if deserializer does not take exactly 1 parameter.", 176 | fn() { 177 | const app = new Application(); 178 | const testDpOak = new DashportOak(app); 179 | 180 | assertThrows(() => testDpOak.authenticate(new TestStrat(), fakeSerializer, () => 'foobar')); 181 | assertThrows(() => testDpOak.authenticate(new TestStrat(), fakeSerializer, (id, testing) => 'foobar')); 182 | }, 183 | }); 184 | 185 | 186 | // Invocation of middleware tests 187 | Deno.test({ 188 | name: "authenticate's middleware should throw an error if _dashport object does not exist on ctx.state.", 189 | fn() { 190 | const app = new Application(); 191 | const testDpOak = new DashportOak(app); 192 | const fakeOakCtx = { 193 | app: {}, 194 | cookies: {}, 195 | request: {}, 196 | respond: {}, 197 | response: {}, 198 | socket: {}, 199 | state: {}, 200 | assert: () => 1, 201 | send: () => 2, 202 | sendEvents: () => 3, 203 | throw: () => 4, 204 | upgrade: () => 5, 205 | params: {} 206 | } 207 | 208 | assertThrowsAsync(async () => await testDpOak.authenticate(new TestStrat(), fakeSerializer, fakeDeserializer)(fakeOakCtx, fakeNext)); 209 | }, 210 | }); 211 | 212 | Deno.test({ 213 | name: "If ctx.state._dashport.session equals dashport._sId, authenticate should store the deserialized user info on ctx.locals and invoke the 'next' function.", 214 | async fn() { 215 | const app = new Application(); 216 | const testDpOak = new DashportOak(app); 217 | const fakeOakCtx = { 218 | app: {}, 219 | cookies: {}, 220 | request: {}, 221 | respond: {}, 222 | response: {}, 223 | socket: {}, 224 | state: { 225 | _dashport: { 226 | session: '123456789' 227 | } 228 | }, 229 | assert: () => 1, 230 | send: () => 2, 231 | sendEvents: () => 3, 232 | throw: () => 4, 233 | upgrade: () => 5, 234 | params: {} 235 | } 236 | 237 | testDpOak._sId = '123456789'; 238 | 239 | assertEquals(await testDpOak.authenticate(new TestStrat(), fakeSerializer, fakeDeserializer)(fakeOakCtx, fakeNext), 'fakeNext was invoked'); 240 | assertEquals(fakeOakCtx.locals, { firstName: 'Hello', lastName: 'World' }); 241 | }, 242 | }); 243 | 244 | Deno.test({ 245 | name: "If there is no session or session ID does not match, authenticate should call the strategy's router method and begin authentication.", 246 | async fn(){ 247 | const app = new Application(); 248 | const testDpOak = new DashportOak(app); 249 | const fakeOakCtx = { 250 | app: {}, 251 | cookies: {}, 252 | request: {}, 253 | respond: {}, 254 | response: {}, 255 | socket: {}, 256 | state: { 257 | _dashport: {}, 258 | testing: 'user' 259 | }, 260 | assert: () => 1, 261 | send: () => 2, 262 | sendEvents: () => 3, 263 | throw: () => 4, 264 | upgrade: () => 5, 265 | params: {} 266 | } 267 | 268 | // if authenticate was successful, the 'next' function should be invoked 269 | assertEquals(await testDpOak.authenticate(new TestStrat(), fakeSerializer, fakeDeserializer)(fakeOakCtx, fakeNext), 'fakeNext was invoked'); 270 | // after authenticate was successful, the serialized ID should now exist on fakeOakCtx.state._dashport.session 271 | assertEquals(fakeOakCtx.state._dashport.session, 'ABCDEF'); 272 | // after authenticate was successful, the serialized ID should now exist on testDpOak._sId 273 | assertEquals(testDpOak._sId, 'ABCDEF'); 274 | // after authenticate was successful, deserialized info should exist on ctx.locals 275 | assertEquals(fakeOakCtx.locals, { firstName: 'Hello', lastName: 'World' }); 276 | }, 277 | }); 278 | 279 | 280 | 281 | // LOGOUT TEST 282 | Deno.test({ 283 | name: "logOut should delete the session property on ctx.state._dashport", 284 | async fn() { 285 | const app = new Application(); 286 | const testDpOak = new DashportOak(app); 287 | const fakeOakCtx = { 288 | app: {}, 289 | cookies: {}, 290 | request: {}, 291 | respond: {}, 292 | response: {}, 293 | socket: {}, 294 | state: { 295 | _dashport: { 296 | session: '123' 297 | } 298 | }, 299 | assert: () => 1, 300 | send: () => 2, 301 | sendEvents: () => 3, 302 | throw: () => 4, 303 | upgrade: () => 5, 304 | params: {} 305 | } 306 | 307 | assertEquals(fakeOakCtx.state._dashport.session, '123'); 308 | await testDpOak.logOut(fakeOakCtx, fakeNext); 309 | assertEquals(fakeOakCtx.state._dashport.session, undefined); 310 | } 311 | }); 312 | -------------------------------------------------------------------------------- /dashports/dashportOak.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "../deps.ts"; 2 | import { AuthData, OakContext, Strategy } from '../types.ts'; 3 | 4 | class DashportOak { 5 | private _sId: (string | number) = ''; 6 | 7 | constructor(app: Application) { 8 | const self = this; 9 | app.use(self.initialize); 10 | } 11 | 12 | /** 13 | * Creates a _dashport object that will persist across multiple HTTP requests. 14 | * 15 | * @param {OakContext} ctx - Oak's 'context' object 16 | * @param {*} next - Oak's 'next' function 17 | */ 18 | public async initialize(ctx: OakContext, next: any) { 19 | // '?.' - optional chaining 20 | if (ctx?.state === undefined) { 21 | throw new Error('ERROR in initialize: ctx object and \'state\' property needs to exist when using Oak. Use app.use(dashport.initialize) not app.use(dashport.initialize()).'); 22 | } 23 | 24 | // if a _dashport object on ctx.state does not exist, create one. ctx.state 25 | // will persist across different requests 26 | if (ctx.state._dashport === undefined) { 27 | ctx.state._dashport = {}; 28 | } 29 | 30 | return await next(); 31 | } 32 | 33 | /** 34 | * In OAuth, there is a lot of back and forth communication between the 35 | * client, the app, and the OAuth provider, when a user want to sign in. 36 | * Tokens are sent back and forth and user data gets sent back at the end. 37 | * The middleware function that is returned from the 'authenticate' method 38 | * bundles up this functionality by executing code depending if a user has 39 | * been authenticated or not. This allows one method to be used for a seamless 40 | * OAuth process. 41 | * 42 | * Authenticate method takes in a 'strategy', 'serializer', and 'deserializer'. 43 | * 44 | * Strategies are classes that handle the OAuth process for a specific OAuth 45 | * provider. A 'strategy' will be an instantiation of a new Strategy class 46 | * such as GoogleStrategy. When strategies are instantiated, options must be 47 | * passed in such as a client ID, client secret, redirect URI, etc., for the 48 | * strategy class to use. ALL strategies made for Dashport MUST have a 'router' 49 | * method that on successful authentication, returns an authData object with a 50 | * userInfo property in the form of UserProfile. 51 | * 52 | * Serializers are functions that need to have the following functionalities: 53 | * 1. The serializer function needs to take in one argument which will be 54 | * the user data in the form of an object. This will be in the shape of 55 | * the UserProfile interface 56 | * 2. The serializer function should to specify how to create a serialized ID 57 | * 3. The serializer function needs to specify what the developer wants to 58 | * do with the user data (e.g. store in a database) 59 | * 4. The serializer function should return the serialized ID or an error 60 | * 61 | * Deserializers are functions that need to have the following functionalities: 62 | * 1. The deserializer function needs to take in one parameter which will be 63 | * the serialized ID 64 | * 2. The deserializer function needs to specify what the developer wants to 65 | * do with the serialized ID to obtain user info (e.g. fetch the userData 66 | * from a database) 67 | * 3. The deserializer function needs to return the user info or an Error 68 | * 69 | * EXAMPLE: Using dashport.authenticate as a middleware in Oak 70 | * import * as dpSettings from './dashport.settings.ts'; 71 | * 72 | * router.get('/test', 73 | * dashport.authenticate( 74 | * dpSettings.googleStrat, 75 | * dpSettings.serializerA, 76 | * dpSettings.deserializerA 77 | * ), 78 | * (ctx, next) => { 79 | * ctx.response.body = 'Hello World'; 80 | * } 81 | * ); 82 | * 83 | * @param {Strategy} strategy - An instance of a strategy to be used 84 | * @param {Function} serializer - The function that will take in user info and 85 | * output an ID 86 | * @param {Function} deserializer - The function that will take in an ID and 87 | * output user info 88 | * @returns {Function} The function that will be the middleware 89 | */ 90 | public authenticate(strategy: Strategy, serializer: Function, deserializer: Function): Function { 91 | // below error checks will only run once when code is first compiled 92 | 93 | // arguments error check 94 | if (strategy === undefined || serializer === undefined || deserializer === undefined) { 95 | throw new Error("ERROR in authenticate: authenticate must be passed 'strategy', 'serializer', and 'deserializer' values."); 96 | } 97 | 98 | // strategy error checks 99 | if (typeof(strategy) !== 'object') { 100 | throw new Error('ERROR in authenticate: strategy must be an object (should be a class).'); 101 | } 102 | if (strategy.router === undefined) { 103 | throw new Error('ERROR in authenticate: strategy must have a \'router\' property.'); 104 | } 105 | if (typeof(strategy.router) !== 'function') { 106 | throw new Error('ERROR in authenticate: strategy\'s \'router\' property must be a function (should be method on strategy class).'); 107 | } 108 | 109 | // serializer error checks 110 | if (typeof serializer !== 'function') { 111 | throw new Error('ERROR in authenticate: serializer must be a function.'); 112 | } 113 | if (serializer.length !== 1) { 114 | throw new Error('ERROR in authenticate: serializer function must take in exactly 1 parameter (which will be an object containing the user info).'); 115 | } 116 | 117 | // deserializer error checks 118 | if (typeof deserializer !== 'function') { 119 | throw new Error('ERROR in authenticate: deserializer must be a function.'); 120 | } 121 | if (deserializer.length !== 1) { 122 | throw new Error('ERROR in authenticate: deserializer function must take in exactly 1 parameter (which will be the ID generated from the serializer).'); 123 | } 124 | 125 | return async (ctx: OakContext, next: any) => { 126 | if (ctx.state._dashport === undefined) { 127 | throw new Error('ERROR in authenticate: Dashport needs to be initialized first with app.use(dashport.initialize).'); 128 | } 129 | 130 | if (ctx.state._dashport.session) { 131 | if (this._sId !== '' && ctx.state._dashport.session === this._sId) { 132 | // if checks pass, then user has been authenticated 133 | const userData: any = await deserializer(this._sId); 134 | ctx.locals = userData; 135 | 136 | return await next(); 137 | } 138 | } 139 | 140 | // if above check is not passed, user must be authenticated (again), so 141 | // call the requested strategy's 'router' method. All strategies must have 142 | // a 'router' method. authData must contain a userInfo property in the 143 | // form of UserProfile 144 | const authData: (AuthData | undefined) = await strategy.router(ctx, next); 145 | 146 | // check if authData is not undefined (it will be undefined in the first 147 | // step of the OAuth process when user gets redirected to OAuth provider 148 | // to log in). If it is not undefined, that means user has been 149 | // authenticated by OAuth. Can begin session management process 150 | if (authData !== undefined) { 151 | // obtain a serialized ID by using the serializer function 152 | const serializedId: (string | number | Error) = await serializer(authData.userInfo); 153 | 154 | // if serializedId is an Error, throw it to be caught 155 | if (serializedId instanceof Error) throw serializedId; 156 | 157 | if (typeof serializedId !== 'string' && typeof serializedId !== 'number') { 158 | throw new Error('ERROR in authenticate: serializedId returned from serializer must be a string, number, or an Error.') 159 | } 160 | 161 | // store the serialized ID onto a session object on _dashport and onto 162 | // this instance of Dashport's _sId 163 | this.logIn(ctx, serializedId); 164 | 165 | // invoke the deserializer, passing in the serialized ID, to get the 166 | // data in the structure the developer wants. Store the user data on 167 | // ctx.locals for the next middleware to access 168 | const userData: any = await deserializer(this._sId); 169 | ctx.locals = userData; 170 | 171 | return await next(); 172 | } 173 | } 174 | } 175 | 176 | /** 177 | * Adds the serializedId obtained in authenticate method to the _dashport 178 | * object and to the _sId property of this instance of Dashport. This will 179 | * help with session management. 180 | * 181 | * @param {OakContext} ctx - the object that contains the _dashport object 182 | * @param {(string|number)} serializedId - the serialized ID generated in 183 | * authenticate by the serializer function 184 | */ 185 | private logIn(ctx: OakContext, serializedId: (string | number)): void { 186 | if (ctx.state._dashport) { 187 | ctx.state._dashport.session = serializedId; 188 | this._sId = serializedId; 189 | } else { 190 | throw new Error('ERROR in logIn: ctx.state._dashport does not exist. Please use app.use(dashport.initialize).'); 191 | } 192 | } 193 | 194 | /** 195 | * Deletes any session info that exists on the _dashport object. This will 196 | * cause the user to go through the OAuth process on the next authenticate call. 197 | * 198 | * @param {OakContext} ctx - the object that contains the _dashport object 199 | * @param {*} next - Oak's 'next' function 200 | */ 201 | public async logOut(ctx: OakContext, next: any) { 202 | delete ctx.state._dashport.session; 203 | return await next(); 204 | } 205 | } 206 | 207 | export default DashportOak; 208 | -------------------------------------------------------------------------------- /dashports/templateDashport.ts: -------------------------------------------------------------------------------- 1 | class TemplateDashport { 2 | private _sId: (string | number) = ''; 3 | 4 | /** 5 | * Usually a middleware function. 6 | * 7 | * Creates a _dashport object that will persist across multiple HTTP requests. 8 | * 9 | * Parameters will be specific to the server framework (e.g. Oak will be ctx 10 | * and next) 11 | * Output will usually be invocation of a 'next' function 12 | */ 13 | public initialize() { 14 | // check if _dashport exists. If it doesn't, create one 15 | // invoke 'next' 16 | } 17 | 18 | /** 19 | * In OAuth, there is a lot of back and forth communication between the 20 | * client, the app, and the OAuth provider, when a user want to sign in. 21 | * Tokens are sent back and forth and user data gets sent back at the end. 22 | * The middleware function that is returned from the 'authenticate' method 23 | * bundles up this functionality by executing code depending if a user has 24 | * been authenticated or not. This allows one method to be used for a seamless 25 | * OAuth process. 26 | * 27 | * Authenticate method takes in a 'strategy', 'serializer', and 'deserializer'. 28 | * 29 | * Strategies are classes that handle the OAuth process for a specific OAuth 30 | * provider. A 'strategy' will be an instantiation of a new Strategy class 31 | * such as GoogleStrategy. When strategies are instantiated, options must be 32 | * passed in such as a client ID, client secret, redirect URI, etc., for the 33 | * strategy class to use. ALL strategies made for Dashport MUST have a 'router' 34 | * method that on successful authentication, returns an authData object with a 35 | * userInfo property in the form of UserProfile. 36 | * 37 | * Serializers are functions that need to have the following functionalities: 38 | * 1. The serializer function needs to take in one argument which will be 39 | * the user data in the form of an object. This will be in the shape of 40 | * the UserProfile interface 41 | * 2. The serializer function needs to specify what the developer wants to 42 | * do with the user data (e.g. store in a database) 43 | * 3. The serializer function should to specify how to create a serialized ID 44 | * 4. The serializer function should return the serialized ID or an error 45 | * 46 | * Deserializers are functions that need to have the following functionalities: 47 | * 1. The deserializer function needs to take in one parameter which will be 48 | * the serialized ID 49 | * 2. The deserializer function needs to specify what the developer wants to 50 | * do with the serialized ID to obtain user info (e.g. fetch user's data 51 | * from a database) 52 | * 3. The deserializer function needs to return the user info or an Error 53 | * 54 | * EXAMPLE: Using dashport.authenticate as a middleware in Oak 55 | * import * as dpSettings from './dashport.settings.ts'; 56 | * 57 | * router.get('/test', 58 | * dashport.authenticate( 59 | * dpSettings.googleStrat, 60 | * dpSettings.serializerA, 61 | * dpSettings.deserializerA 62 | * ), 63 | * (ctx, next) => { 64 | * ctx.response.body = 'Hello World'; 65 | * } 66 | * ); 67 | * 68 | * @param strategy - An instance of a strategy to be used 69 | * @param {Function} serializer - The function that will take in user info and 70 | * output an ID 71 | * @param {Function} deserializer - The function that will take in an ID and 72 | * output user info 73 | * @returns Output should be the function that will be the middleware 74 | */ 75 | public authenticate(strategy, serializer: Function, deserializer: Function) { 76 | // below error checks will only run once when code is first compiled 77 | 78 | // arguments error check 79 | if (strategy === undefined || serializer === undefined || deserializer === undefined) { 80 | throw new Error("ERROR in authenticate: authenticate must be passed 'strategy', 'serializer', and 'deserializer' values."); 81 | } 82 | 83 | // strategy error checks 84 | if (typeof(strategy) !== 'object') { 85 | throw new Error('ERROR in authenticate: strategy must be an object (should be a class).'); 86 | } 87 | if (strategy.router === undefined) { 88 | throw new Error('ERROR in authenticate: strategy must have a \'router\' property.'); 89 | } 90 | if (typeof(strategy.router) !== 'function') { 91 | throw new Error('ERROR in authenticate: strategy\'s \'router\' property must be a function (should be method on strategy class).'); 92 | } 93 | 94 | // serializer error checks 95 | if (typeof serializer !== 'function') { 96 | throw new Error('ERROR in authenticate: serializer must be a function.'); 97 | } 98 | if (serializer.length !== 1) { 99 | throw new Error('ERROR in authenticate: serializer function must take in exactly 1 parameter (which will be an object containing the user info).'); 100 | } 101 | 102 | // deserializer error checks 103 | if (typeof deserializer !== 'function') { 104 | throw new Error('ERROR in authenticate: deserializer must be a function.'); 105 | } 106 | if (deserializer.length !== 1) { 107 | throw new Error('ERROR in authenticate: deserializer function must take in exactly 1 parameter (which will be the ID generated from the serializer).'); 108 | } 109 | 110 | // return an async middleware function that fits the server framework being used 111 | // check if _dashport object exists. If it doesn't, throw an error 112 | 113 | // check if session property exists on _dashport. If it does, check if its 114 | // value equals this._sId and that this._sId is not ''. If true user has 115 | // already been authenticated, so invoke deserializer and store info on 116 | // ctx.local for next middleware, and call 'next' 117 | 118 | // if above check is not passed, user must be authenticated (again), so 119 | // call the requested strategy's 'router' method. authData must contain 120 | // a userInfo property in the form of UserProfile 121 | // ex for Oak: 122 | // const authData: (AuthData | undefined) = await strategy.router(ctx, next); 123 | 124 | // check if authData is not undefined (it will be undefined in the first 125 | // step of the OAuth process when user gets redirected to OAuth provider 126 | // to log in). If it is not undefined, that means user has been 127 | // authenticated by OAuth. Can begin session management process 128 | // obtain a serialized ID by using the serializer function. It should be 129 | // a string or a number 130 | 131 | // store the serialized ID onto a session object on _dashport and onto 132 | // this instance of Dashport's _sId 133 | 134 | // invoke the deserializer, passing in the serialized ID, to get the 135 | // data in the structure the developer wants. Store the info on 136 | // ctx.locals for the next middleware to access 137 | 138 | // invoke 'next' 139 | } 140 | 141 | /** 142 | * Adds the serializedId obtained in authenticate method to the _dashport 143 | * object and to the _sId property of this instance of Dashport. This will 144 | * help with session management. 145 | * 146 | * One parameter will be the object containing the _dashport object 147 | * @param {(string|number)} serializedId - the serialized ID generated in 148 | * authenticate by the serializer function 149 | */ 150 | private logIn(serializedId: (string | number)): void { 151 | // check if the _dashport object exists 152 | // if it does, add serializedId as a value onto _dashport and as the value 153 | // for this._sId 154 | // if it doesn't, throw an error since _dashport needs to exist 155 | } 156 | 157 | /** 158 | * Deletes any session info that exists on the _dashport object. This will 159 | * cause the user to go through the OAuth process on the next authenticate call. 160 | * 161 | * Parameters will be specific to the server framework (e.g. Oak will be ctx 162 | * and next) 163 | */ 164 | public logOut() { 165 | // delete the session information from _dashport 166 | // invoke 'next' 167 | } 168 | } 169 | 170 | export default TemplateDashport; 171 | -------------------------------------------------------------------------------- /deprecated/v1.0.1/dashport.ts: -------------------------------------------------------------------------------- 1 | import { OakContext, Translators, Strategies, Strategy } from './types.ts'; 2 | import SessionManager from './sessionManager.ts'; 3 | 4 | class Dashport { 5 | private _serializers: Translators = {}; 6 | private _deserializers: Translators = {}; 7 | private _strategies: Strategies = {}; 8 | private _framework: string; 9 | private _sm: SessionManager; 10 | // public since _sId needs to be accessed by SessionManager but _ since a 11 | // developer should not access it 12 | public _sId: string = ''; 13 | public initialize: any; 14 | // Note to help clear any confusion - the purpose of deserialize is NOT to 15 | // remove the serializedId. High-level explanation: 16 | // serialize takes in user info and outputs a serializedId 17 | // deserialize takes in a serializedId and outputs user info 18 | public deserialize: any; 19 | public logOut: Function; 20 | 21 | constructor(frmwrk: string) { 22 | frmwrk = frmwrk.toLowerCase(); 23 | this._framework = frmwrk; 24 | this._sm = new SessionManager(frmwrk); 25 | this.logOut = this._sm.logOut; 26 | this.initialize = this._initializeDecider(frmwrk); 27 | this.deserialize = this._deserializeDecider(frmwrk); 28 | this.authenticate = this.authenticate.bind(this); 29 | } 30 | 31 | /** 32 | * Takes in a framework and returns a function that will become dashport's 33 | * initialize method. _initializeDecider runs inside the constructor of a new 34 | * Dashport instance. 35 | * 36 | * TODO: Add other frameworks 37 | * 38 | * @param {string} framework - The server framework that will be used 39 | * @returns {*} The async function that will be dashport's intialize method 40 | */ 41 | private _initializeDecider(framework: string) { 42 | if (framework === 'oak') { 43 | return async (ctx: OakContext, next: any) => { 44 | if (ctx.state === undefined) { 45 | throw new Error('ERROR in initialize: \'state\' property needs to exist on context object of Oak. Use app.use(dashport.initialize) not app.use(dashport.initialize()).'); 46 | } 47 | 48 | // if the _dashport property on ctx.state does not exist, create one. 49 | // ctx.state will persist across requests 50 | if (ctx.state._dashport === undefined) { 51 | ctx.state._dashport = {}; 52 | } 53 | 54 | return await next(); 55 | } 56 | } 57 | 58 | throw new Error('ERROR in constructor of Dashport: Name of framework passed in is not supported.'); 59 | } 60 | 61 | /** 62 | * Takes in a strategy name that should exist on this._strategies. The 63 | * strategy will need to be have been added by the developer. 64 | * 65 | * Authenticate returns an async function depending on the framework being 66 | * used, that will serve as a middleware function. 67 | * 68 | * In OAuth, there is a lot of back and forth communication between the 69 | * client, the app, and the OAuth provider, when a user want to sign in. 70 | * Tokens are sent back and forth and user data gets sent back at the end. 71 | * The middleware function that is returned from the 'authenticate' method 72 | * bundles up this functionality by executing code depending if a user has 73 | * been authenticated or not. This allows one method to be used for a seamless 74 | * OAuth process. 75 | * 76 | * EXAMPLE: Adding a strategy 77 | * 78 | * dashport.addStrategy('google', GoogleStrategy); 79 | * 80 | * EXAMPLE: Using dashport.authenticate as a middleware in Oak 81 | * 82 | * router.get('/test', 83 | * dashport.authenticate('google'), 84 | * (ctx: OakContext, next: any) => { 85 | * ctx.response.body = 'Hello World'; 86 | * } 87 | * ); 88 | * 89 | * TODO: Add optional parameter for options in case developers want to have 90 | * different strategy options for a particular route. 91 | * 92 | * @param {string} stratName - The name of a strategy that was added 93 | * @returns {Function} The middleware function (differs depending on server framework) 94 | */ 95 | public authenticate(stratName: string): Function { 96 | const self: Dashport = this; 97 | 98 | if (this._strategies[stratName] === undefined) { 99 | throw new Error('ERROR in authenticate: This strategy name has not been specified for use.'); 100 | } 101 | 102 | if (this._framework === 'oak') { 103 | return async (ctx: OakContext, next: any) => { 104 | if (ctx.state._dashport === undefined) { 105 | throw new Error('ERROR in authenticate: Dashport needs to be initialized first with app.use(dashport.initialize).'); 106 | } 107 | 108 | // check if a session object exists (created by SessionManager.logIn). 109 | // If it exists, check if the session ID matches. If it does, user has 110 | // already been authenticated, so user can go to next middleware 111 | if (ctx.state._dashport.session) { 112 | if (ctx.state._dashport.session === self._sId) { 113 | return await next(); 114 | } 115 | } 116 | 117 | // if above check is not passed, user must be authenticated (again), so 118 | // call the requested strategy's 'router' method. authData must contain 119 | // a userInfo property in the form of UserProfile 120 | const authData: any = await self._strategies[stratName].router(ctx, next); 121 | 122 | if (authData !== undefined) { 123 | // serializedId will be obtained by calling SessionManager's serialize 124 | // function, which will invoke the serializer(s) the developer specified. 125 | // serializedId is type 'any' because lefthand side of instanceof must 126 | // be type 'any' 127 | const serializedId: any = await self._sm.serialize(self._serializers, authData.userInfo); 128 | 129 | // if serializedId is an Error, throw it to be caught 130 | if (serializedId instanceof Error) throw serializedId; 131 | if (typeof serializedId !== 'string' && typeof serializedId !== 'number') { 132 | throw new Error('ERROR in authenticate: serializedId returned from serializer must be a string, number, or an Error.') 133 | } 134 | 135 | // use SessionManager's logIn method to create a session object on 136 | // ctx.state._dashport and to assign serializedId to the _sId property 137 | // of this instance of Dashport 138 | self._sm.logIn(ctx, self, serializedId); 139 | 140 | return await next(); 141 | } 142 | } 143 | } 144 | 145 | throw new Error('ERROR in authenticate: Name of current framework is not supported.'); 146 | } 147 | 148 | /** 149 | * Takes in a name for a serializer function and the serializer function the 150 | * developer specifies. Serializer function needs to do 4 things below: 151 | * 152 | * 1. The serializer function needs to take in one parameter which will be the 153 | * user data in the form of an object. 154 | * 2. The serializer function needs to specify what the developer wants to do 155 | * with the user data (store it somewhere, add some info to response body, 156 | * etc). 157 | * 3. The serializer function should to specify how to create a serialized ID. 158 | * 4. The serializer function should return the serialized ID or an error. 159 | * 160 | * EXAMPLE 161 | * 162 | * dashport.addSerializer('1', (userInfo) => { 163 | * const serializedId = Math.random() * 10000; 164 | * 165 | * try { 166 | * // do something with userInfo like store in a database 167 | * return serializedId; 168 | * } catch(err) { 169 | * // err should be an instance of 'Error' 170 | * return err; 171 | * } 172 | * }); 173 | * 174 | * @param {string} serializerName - A name to give the serializer if it needs 175 | * to be deleted later 176 | * @param {Function} serializer - The function that will create serialized IDs 177 | */ 178 | public addSerializer(serializerName: string, serializer: Function): void { 179 | if (serializer.length !== 1) { 180 | throw new Error('ERROR in addSerializer: Serializer function must have 1 parameter that is the userInfo.'); 181 | } 182 | 183 | // the below if statement is currently not needed. TODO in SessionManager.serialize method 184 | // if (serializerName === 'all') { 185 | // throw new Error('ERROR in addSerializer: Cannot use the name \'all\'. It is a reserved keyword Dashport uses.') 186 | // } 187 | 188 | if (this._serializers[serializerName] !== undefined) { 189 | throw new Error('ERROR in addSerializer: A serializer with this name already exists.'); 190 | } 191 | 192 | this._serializers[serializerName] = serializer; 193 | } 194 | 195 | /** 196 | * Removes a serializer from the this._serializers object. 197 | * 198 | * EXAMPLE 199 | * 200 | * dashport.removeSerializer('1'); 201 | * 202 | * @param {string} serializerName - The name of the serializer to remove 203 | */ 204 | public removeSerializer(serializerName: string): void { 205 | if (this._serializers[serializerName] === undefined) { 206 | throw new Error('ERROR in removeSerializer: The specified serializer does not exist.'); 207 | } 208 | 209 | delete this._serializers[serializerName]; 210 | } 211 | 212 | /** 213 | * Takes in a framework and returns a function that will become dashport's 214 | * deserialize method. _deserializeDecider runs inside the constructor of a 215 | * new Dashport instance. 216 | * 217 | * dashport.deserialize will act as middleware and invoke the specified 218 | * deserializer(s) if the serialized ID on the session object matches the 219 | * serialized ID on the current instance of Dashport. 220 | * 221 | * EXAMPLE: Using Oak as server framework 222 | * 223 | * // Below code written in a dashport configuration file 224 | * dashport.addDeserializer('A', (serializedId) => { 225 | * // code to specify how to obtain user info from serialized ID 226 | * }) 227 | * 228 | * // Below code written in a router file 229 | * router.get('/iWantUserInfo', 230 | * dashport.deserialize('A'), 231 | * (ctx: OakContext, next: any) => { 232 | * ctx.response.body = 'Data was deserialized in previous middleware'; 233 | * } 234 | * ) 235 | * 236 | * TODO: Add other frameworks 237 | * 238 | * TODO: Current deserialize method for Oak uses the first deserializer in 239 | * _deserializers. Extend code to take in an extra parameter (a name) that 240 | * specifies which deserializer to use. 241 | * 242 | * @param {string} framework - The server framework that will be used 243 | * @returns {*} The async function that will be dashport's deserialize method 244 | */ 245 | private _deserializeDecider(framework: string) { 246 | const self: Dashport = this; 247 | 248 | if (framework === 'oak') { 249 | return async (ctx: OakContext, next: any) => { 250 | if (Object.values(self._deserializers).length === 0) { 251 | throw new Error('ERROR in deserialize: No deserializers.'); 252 | } 253 | 254 | let userInfo: any; 255 | 256 | if (ctx.state._dashport.session === undefined) { 257 | userInfo = new Error('ERROR in deserialize: No session exists'); 258 | } else if (self._sId === ctx.state._dashport.session) { 259 | // a deserializer should either return the user info in an object or 260 | // an Error 261 | userInfo = await Object.values(self._deserializers)[0](ctx.state._dashport.session); 262 | } else { 263 | userInfo = new Error('ERROR in deserialize: serializedId cannot be authenticated'); 264 | } 265 | 266 | // store the userInfo or the error in ctx.locals for next middleware to 267 | // access 268 | ctx.locals = userInfo; 269 | return await next(); 270 | } 271 | } 272 | 273 | throw new Error('ERROR in _deserializeDecider: Name of current framework is not supported.'); 274 | } 275 | 276 | /** 277 | * Takes in a name for a deserializer function and the deserializer function 278 | * the developer specifies. Deserializer function needs to do 3 things below: 279 | * 280 | * 1. The deserializer function needs to take in one parameter which will be 281 | * the serialized ID. 282 | * 2. The deserializer function needs to specify what the developer wants to 283 | * do with the serialized ID to obtain user info (e.g. fetch the userData from 284 | * a database). 285 | * 3. The deserializer function needs to return the user info or an Error. 286 | * 287 | * EXAMPLE 288 | * 289 | * dashport.addDeserializer('A', (serializedId) => { 290 | * // handle getting userInfo from a serializedId here. e.g. taking the ID 291 | * // and querying a DB for the info. If userInfo comes back successfully, 292 | * // return it. Otherwise return an error 293 | * try { 294 | * const userInfo = await (look up serializedId in a db here); 295 | * return userInfo; 296 | * } catch(err) { 297 | * return err; 298 | * } 299 | * }) 300 | * 301 | * @param {string} deserializerName - A name to give the deserializer if it 302 | * needs to be deleted later 303 | * @param {Function} deserializer - The function that will take a serialized ID 304 | * and return the user info in an object or an Error 305 | */ 306 | public addDeserializer(deserializerName: string, deserializer: Function): void { 307 | if (deserializer.length !== 1) { 308 | throw new Error('ERROR in addDeserializer: Deserializer function must have 1 parameter that is the serializedId.'); 309 | } 310 | 311 | // the below if statement is currently not needed. TODO in Dashport._deserializeDecider method 312 | // if (deserializerName === 'all') { 313 | // throw new Error('ERROR in addDeserializer: Cannot use the name \'all\'. It is a reserved keyword Dashport uses.') 314 | // } 315 | 316 | if (this._deserializers[deserializerName] !== undefined) { 317 | throw new Error('ERROR in addDeserializer: A deserializer with this name already exists.'); 318 | } 319 | 320 | this._deserializers[deserializerName] = deserializer; 321 | } 322 | 323 | /** 324 | * Removes a deserializer from the this._deserializers object. 325 | * 326 | * EXAMPLE 327 | * 328 | * dashport.removeDeserializer('A'); 329 | * 330 | * @param {string} deserializerName - The name of the serializer to remove 331 | */ 332 | public removeDeserializer(deserializerName: string): void { 333 | if (this._deserializers[deserializerName] === undefined || this._deserializers[deserializerName] === null) { 334 | throw new Error('ERROR in removeDeserializer: The specified deserializer does not exist.'); 335 | } 336 | 337 | delete this._deserializers[deserializerName]; 338 | } 339 | 340 | /** 341 | * Adds an OAuth strategy that the developer would like to use. 342 | * 343 | * EXAMPLE 344 | * 345 | * dashport.addStrategy('google', new GoogleStrategy()); 346 | * 347 | * @param {string} stratName - The name that will be used to reference this strategy 348 | * @param {Class} strategy - The imported OAuth strategy module to be used 349 | */ 350 | public addStrategy(stratName: string, strategy: Strategy): void { 351 | if (stratName === undefined || strategy === undefined) { 352 | throw new Error('ERROR in addStrategy: A strategy name and a strategy must be provided.'); 353 | } 354 | 355 | // ALL strategies made for Dashport MUST have a 'router' method that on 356 | // successful authentication, returns an authData object with a userInfo 357 | // property in the form of UserProfile 358 | if (typeof(strategy.router) !== 'function') { 359 | throw new Error('ERROR in addStrategy: This strategy does not have a \'router\' method.'); 360 | } 361 | 362 | if (this._strategies[stratName] !== undefined) { 363 | throw new Error('ERROR in addStrategy: This strategy already exists.'); 364 | } 365 | 366 | this._strategies[stratName] = strategy; 367 | } 368 | 369 | /** 370 | * Removes an OAuth strategy from the _strategies attribute. 371 | * 372 | * EXAMPLE 373 | * 374 | * dashport.removeStrategy('google'); 375 | * 376 | * @param {string} stratName - The name of the strategy to be deleted 377 | */ 378 | public removeStrategy(stratName: string): void { 379 | if (stratName === undefined) { 380 | throw new Error('ERROR in removeStrategy: A strategy name must be provided.'); 381 | } 382 | 383 | if (this._strategies[stratName] === undefined) { 384 | throw new Error('ERROR in removeStrategy: The specified strategy does not exist.'); 385 | } 386 | 387 | delete this._strategies[stratName]; 388 | } 389 | } 390 | 391 | export default Dashport; -------------------------------------------------------------------------------- /deprecated/v1.0.1/sessionManager.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertNotEquals } from '../../deps.ts'; 2 | import SessionManager from './sessionManager.ts'; 3 | import Dashport from './dashport.ts'; 4 | import { OakContext, Translators, UserProfile } from "../types.ts"; 5 | 6 | const oakTestSM = new SessionManager('oak'); 7 | 8 | Deno.test({ 9 | name: "A new SessionManager should have the properties logIn, logOut, and serialize", 10 | fn(): void { 11 | assertNotEquals(oakTestSM.logIn, undefined); 12 | assertNotEquals(oakTestSM.logOut, undefined); 13 | assertNotEquals(oakTestSM.serialize, undefined); 14 | } 15 | }); 16 | 17 | Deno.test({ 18 | name: "When invoked, logIn method should (1) change the value of session property on ctx.state._dashport to the serializedId passed in and " + 19 | "(2) change the _sId property on dashport to the serializedId passed in", 20 | fn(): void { 21 | const oakTestDp = new Dashport('oak'); 22 | const fakeOakCtx: OakContext = { 23 | app: {}, 24 | cookies: {}, 25 | request: {}, 26 | respond: {}, 27 | response: {}, 28 | socket: {}, 29 | state: { 30 | _dashport: { 31 | session: '' 32 | } 33 | }, 34 | assert: () => 1, 35 | send: () => 2, 36 | sendEvents: () => 3, 37 | throw: () => 4, 38 | upgrade: () => 5, 39 | params: {} 40 | } 41 | 42 | assertEquals(fakeOakCtx.state._dashport.session, ''); 43 | assertEquals(oakTestDp._sId, ''); 44 | oakTestSM.logIn(fakeOakCtx, oakTestDp, '1234567890'); 45 | assertEquals(fakeOakCtx.state._dashport.session, '1234567890'); 46 | assertEquals(oakTestDp._sId, '1234567890'); 47 | } 48 | }); 49 | 50 | Deno.test({ 51 | name: "When invoked, logOut method should delete the session property on ctx.state._dashport", 52 | async fn(): Promise { 53 | const fakeOakCtx: OakContext = { 54 | app: {}, 55 | cookies: {}, 56 | request: {}, 57 | respond: {}, 58 | response: {}, 59 | socket: {}, 60 | state: { 61 | _dashport: { 62 | session: '123' 63 | } 64 | }, 65 | assert: () => 1, 66 | send: () => 2, 67 | sendEvents: () => 3, 68 | throw: () => 4, 69 | upgrade: () => 5, 70 | params: {} 71 | } 72 | const fakeNext: any = () => 'fakeNext was invoked'; 73 | 74 | assertEquals(fakeOakCtx.state._dashport.session, '123'); 75 | await oakTestSM.logOut(fakeOakCtx, fakeNext); 76 | assertEquals(fakeOakCtx.state._dashport.session, undefined); 77 | } 78 | }); 79 | 80 | Deno.test({ 81 | name: "When invoked, serialize method should execute a serializer function from an object of serializers and return a serialized ID", 82 | fn(): void { 83 | const testProfile: UserProfile = { 84 | provider: 'google', 85 | providerUserId: '0987654321', 86 | displayName: 'Dashport', 87 | name: { 88 | familyName: 'port', 89 | givenName: 'Dash' 90 | } 91 | }; 92 | 93 | const fakeDB: any = {}; 94 | 95 | function testFunc(userInfo: UserProfile) { 96 | fakeDB['test'] = userInfo 97 | return '2468'; 98 | }; 99 | 100 | const testSerializers: Translators = { 101 | '1': testFunc 102 | }; 103 | 104 | // fakeDB should be empty 105 | assertEquals(fakeDB, {}); 106 | // running serialize should output a serialized ID ('2468') 107 | assertEquals(oakTestSM.serialize(testSerializers, testProfile), '2468'); 108 | // after running serialize, fakeDB should be filled with the test info 109 | assertEquals(fakeDB, { 110 | test: { 111 | provider: 'google', 112 | providerUserId: '0987654321', 113 | displayName: 'Dashport', 114 | name: { 115 | familyName: 'port', 116 | givenName: 'Dash' 117 | } 118 | } 119 | }); 120 | } 121 | }); 122 | -------------------------------------------------------------------------------- /deprecated/v1.0.1/sessionManager.ts: -------------------------------------------------------------------------------- 1 | import { OakContext, Translators, UserProfile } from './types.ts'; 2 | import Dashport from './dashport.ts'; 3 | 4 | /** 5 | * Takes in the framework to use for SessionManager 6 | * 7 | * EXAMPLE: 8 | * 9 | * const sm = new SessionManager('oak'); 10 | * 11 | * When using an instance of SessionManager, only use 12 | * sm.logIn(); 13 | * sm.logOut(); 14 | * sm.serialize(); 15 | * 16 | * @param {string} framework - The name of the server framework to be used 17 | */ 18 | class SessionManager { 19 | public logIn: Function; 20 | public logOut: Function; 21 | 22 | constructor(framework: string) { 23 | this.logIn = this._logInDecider(framework); 24 | this.logOut = this._logOutDecider(framework); 25 | } 26 | 27 | /** 28 | * Takes in the name of the framework the developer is using and returns a 29 | * function that will add a session object onto the browser's http object. 30 | * This will allow information across different requests to persist. 31 | * 32 | * TODO: Add an optional parameter on the returned function that takes in an 33 | * expiration date for the session 34 | * TODO: Add other frameworks 35 | * 36 | * @param {string} framework - Name of the framework the developer is using 37 | * @returns {Function} The function that adds the dashport.session object onto 38 | * the browser's http object 39 | */ 40 | private _logInDecider(framework: string): Function { 41 | if (framework = 'oak') { 42 | return function(ctx: OakContext, dashport: Dashport, serializedId: string): void { 43 | if (ctx.state._dashport) { 44 | ctx.state._dashport.session = serializedId; 45 | dashport._sId = serializedId; 46 | } else throw new Error('ERROR in _logInDecider: ctx.state._dashport does not exist. Please use app.use(dashport.initialize).') 47 | } 48 | } 49 | 50 | throw new Error('ERROR in _logInDecider: Name of framework passed in is not supported.'); 51 | } 52 | 53 | /** 54 | * Takes in the name of the framework the developer is using and returns a 55 | * middleware that will delete the session object on the browser's http object 56 | * 57 | * TODO: Add other frameworks 58 | * 59 | * @param {string} framework - Name of the framework the developer is using 60 | * @returns {Function} The middleware that deletes the session object from the 61 | * browser's http object 62 | */ 63 | private _logOutDecider(framework: string): Function { 64 | if (framework = 'oak') { 65 | return async (ctx: OakContext, next: any) => { 66 | delete ctx.state._dashport.session; 67 | await next(); 68 | } 69 | } 70 | 71 | throw new Error('ERROR in _logOutDecider: Name of framework passed in is not supported.'); 72 | } 73 | 74 | /** 75 | * Takes in an object of serializer functions and currently - read TODO - uses 76 | * the first one to create a serialized ID and return it. 77 | * 78 | * TODO: Allow a 'name' parameter to be passed in that specifies which 79 | * serializer to use 80 | * TODO: If name === 'all', use all the serializers in a chain. 81 | * 82 | * TODO: Allow optional parameters to be passed into the serializer to be 83 | * used. If chaining multiple serializers is implemented, pass params into the 84 | * first serializer function. 85 | * 86 | * @param {Object} serializers - An object containing serializer functions 87 | * @param {Object} userData - An object containing the authenticated user data 88 | * @returns {string} A serialized ID 89 | */ 90 | public serialize(serializers: Translators, userData: UserProfile): string { 91 | if (Object.values(serializers).length === 0) { 92 | throw new Error('ERROR in serialize: No serializers.'); 93 | } 94 | 95 | // Object.values(serializers)[0] returns the first key/value pair's 96 | // value. We are then invoking it (since it should be a function) and 97 | // returning what's returned (should be a serialized ID) 98 | return Object.values(serializers)[0](userData); 99 | } 100 | } 101 | 102 | export default SessionManager; 103 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { Application } from "https://deno.land/x/oak/mod.ts" 2 | 3 | export { 4 | assertEquals, 5 | assertThrows, 6 | assertThrowsAsync, 7 | } from "https://deno.land/std@0.87.0/testing/asserts.ts"; 8 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import DashportOak from './dashports/dashportOak.ts'; 2 | export { DashportOak }; 3 | -------------------------------------------------------------------------------- /templateStrategy.ts: -------------------------------------------------------------------------------- 1 | import { OakContext, StrategyOptions, AuthData } from '../types.ts'; 2 | 3 | /** 4 | * Creates an instance of `TemplateStrategy`. 5 | * 6 | * 7 | * * Options: 8 | * 9 | * - client_id: string Required 10 | * - client_secret: string Required 11 | * - redirect_uri: string Required 12 | * 13 | */ 14 | export default class TemplateStrategy { 15 | name: string = ''; 16 | options: StrategyOptions; 17 | uriFromParams: string; 18 | // ACTION NEEDED: 19 | // add the url for the first endpoint here 20 | authURL: string = '' 21 | // add the url to exchange the auth code for a token here 22 | tokenURL: string = '' 23 | // add the url to exchange the token for auth data here 24 | authDataURL: string = '' 25 | /** 26 | * @constructor 27 | * @param {Object} options 28 | * @api public 29 | */ 30 | constructor (options: StrategyOptions) { 31 | // customize with whatever fields are required to send to first redirect 32 | if (!options.client_id || !options.redirect_uri || !options.client_secret) { 33 | throw new Error('ERROR in TemplateStrategy constructor: Missing required arguments'); 34 | } 35 | 36 | this.options = options; 37 | 38 | // PRE STEP 1: 39 | // Constructs the second half of the authURL for developer's first endpoint from the info put into 'options' 40 | // ACTION NEEDED: 41 | // If there are any variables in options that aren't needed for developer's first endpoint (but will be needed later), 42 | // add them as an array of strings (even if there's only 1 item) 43 | this.uriFromParams = this.constructURI(this.options); 44 | } 45 | 46 | constructURI(options: any, skip?: string[]): any { 47 | const paramArray: string[][] = Object.entries(options); 48 | let paramString: string = ''; 49 | 50 | for (let i = 0; i < paramArray.length; i++) { 51 | let [key, value] = paramArray[i]; 52 | 53 | if (skip && skip.includes(key)) continue; 54 | // adds the key and '=' for every member of options not in the skip array 55 | paramString += (key + '='); 56 | // adds the value and '&' for every member of options not in the skip array 57 | paramString += (value + '&'); 58 | } 59 | 60 | // removes the '&' that was just placed at the end of the string 61 | if (paramString[paramString.length - 1] === '&') { 62 | paramString = paramString.slice(0, -1); 63 | } 64 | 65 | return paramString; 66 | } 67 | 68 | // parses an encoded URI 69 | parseCode(encodedCode: string): string { 70 | const replacements: { [name: string] : string } = { 71 | "%24": "$", 72 | "%26": "&", 73 | "%2B": "+", 74 | "%2C": ",", 75 | "%2F": "/", 76 | "%3A": ":", 77 | "%3B": ";", 78 | "%3D": "=", 79 | "%3F": "?", 80 | "%40": "@" 81 | } 82 | const toReplaceArray: string[] = Object.keys(replacements); 83 | 84 | for (let i = 0; i < toReplaceArray.length; i++) { 85 | while (encodedCode.includes(toReplaceArray[i])) { 86 | encodedCode = encodedCode.replace(toReplaceArray[i], replacements[toReplaceArray[i]]); 87 | } 88 | } 89 | 90 | return encodedCode; 91 | } 92 | 93 | // ENTRY POINT 94 | async router(ctx: OakContext, next: any) { 95 | // GO_Step 1 Request Permission 96 | if (!ctx.request.url.search) return await this.authorize(ctx, next); 97 | // GO_Step 3 Exchange code for Token 98 | // ACTION REQUIRED: verify that a successful response from getAuthToken includes 'code' in the location specified below 99 | if (ctx.request.url.search.slice(1, 5) === 'code') return this.getAuthToken(ctx, next); 100 | } 101 | 102 | // STEP 1: sends the programatically constructed uri to an OAuth 2.0 server 103 | async authorize(ctx: OakContext, next: any) { 104 | return await ctx.response.redirect(this.authURL + this.uriFromParams); 105 | } 106 | 107 | // STEP 2: client says yes or no 108 | 109 | // STEP 3: handle OAuth 2.0 server response containing auth code 110 | // STEP 3.5: request access token in exchange for auth code 111 | async getAuthToken(ctx: OakContext, next: any) { 112 | // the URI sent back to the endpoint provided in step 1 113 | const OGURI: string = ctx.request.url.search; 114 | 115 | if (OGURI.includes('error')) { 116 | return new Error('ERROR in getAuthToken: Received an error from auth token code request.'); 117 | } 118 | 119 | // EXTRACT THE AUTH CODE 120 | // ACTION REQUIRED: verify that this function works for the format of the response received. uncomment the line below to test: 121 | // console.log('AUTH RESPONSE:', OGURI); 122 | // splits the string at the '=,' storing the first part in URI1[0] and the part wanted in URI1[1] 123 | let URI1: string[] = OGURI.split('='); 124 | // splits the string at the '&', storing the string with the access_token in URI2[0] 125 | // and the other parameters at URI2[n] 126 | const URI2: string[] = URI1[1].split('&'); 127 | // PARSE THE URI 128 | const code: string = this.parseCode(URI2[0]); 129 | 130 | // STEP 3.5 131 | // ACTION REQUIRED: add or remove the parameters needed to send as response to token request 132 | const tokenOptions: any = { 133 | client_id: this.options.client_id, 134 | redirect_uri: this.options.redirect_uri, 135 | client_secret: this.options.client_secret, 136 | code: code, 137 | } 138 | 139 | // SEND A FETCH REQ FOR TOKEN 140 | try { 141 | let data: any = await fetch(this.tokenURL + this.constructURI(tokenOptions)); 142 | data = await data.json(); 143 | 144 | if (data.type === 'oAuthException') { 145 | return new Error('ERROR in getAuthToken: Token request threw OAuth exception.'); 146 | } 147 | 148 | // PASSES TOKEN ON TO STEP 4 149 | return this.getAuthData(data); 150 | } catch(err) { 151 | return new Error(`ERROR in getAuthToken: Unable to obtain token - ${err}`); 152 | } 153 | } 154 | 155 | // STEP 4 get the access token from the returned data 156 | // STEP 4.5 exchange access token for user info 157 | async getAuthData(parsed: any){ 158 | // ACTION REQUIRED: 159 | // fill in the fields for tokenData based on the token obj obtained in the last step 160 | // authData is what is going to be passed back to dashport's authenticate method 161 | const authData: AuthData = { 162 | tokenData: { 163 | access_token: parsed.access_token, 164 | token_type: parsed.token_type, 165 | expires_in: parsed.expires_in, 166 | }, 167 | userInfo: { 168 | provider: '', 169 | providerUserId: '' 170 | } 171 | } 172 | 173 | // STEP 4.5: request user info 174 | // ACTION REQUIRED: 175 | // fill in the fields for auth options with the information required by requested OAuth service 176 | // authOptions constructs the uri for the final fetch request 177 | const authOptions: any = { 178 | input_token: authData.tokenData.access_token, 179 | access_token: this.options.client_id + '|' + this.options.client_secret 180 | }; 181 | 182 | try { 183 | let data: any = await fetch(this.authDataURL + this.constructURI(authOptions)); 184 | data = await data.json(); 185 | 186 | // ACTION REQUIRED: 187 | // Add any data to pass back to dashport's authenticate method here 188 | authData.userInfo = { 189 | provider: this.name, 190 | providerUserId: data.data.user_id 191 | }; 192 | 193 | return authData; 194 | } catch(err) { 195 | return new Error(`ERROR in getAuthData: Unable to obtain auth data - ${err}`); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /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 | * All Dashport strategies are classes that must contain a router method 24 | */ 25 | export interface Strategy { 26 | router: Function; 27 | } 28 | 29 | /** 30 | * Different OAuths will return different user information in different 31 | * structures. Dashport strategies should break down and reconstruct the user 32 | * info into the standardized UserProfile below 33 | */ 34 | export interface UserProfile { 35 | // the provider the user is authenticated with 36 | provider: string; 37 | // the unique id a user has with that specific provider 38 | providerUserId: string; 39 | // the display name or username for this specific user 40 | displayName?: string; 41 | name?: { 42 | familyName?: string; 43 | givenName?: string; 44 | middleName?: string; 45 | }; 46 | emails?: Array; 47 | } 48 | 49 | /** 50 | * All OAuth 2.0 providers will provide access tokens 51 | */ 52 | export interface TokenData { 53 | access_token: string; 54 | [options: string]: string; 55 | } 56 | 57 | /** 58 | * The form the information from strategies should come back in 59 | */ 60 | export interface AuthData { 61 | tokenData: TokenData; 62 | userInfo: UserProfile; 63 | } 64 | 65 | /** 66 | * At the bare minimum, OAuth 2.0 providers will require a client ID, client 67 | * secret, and redirect URI. The remaining options depend on the OAuth 2.0 68 | * provider, such as scope or state 69 | */ 70 | export interface StrategyOptions { 71 | client_id: string; 72 | client_secret: string; 73 | redirect_uri: string; 74 | [option: string]: string; 75 | } 76 | --------------------------------------------------------------------------------