UNPKG

12.7 kBMarkdownView Raw
1# Authpal
2
3authpal
4
5[![NPM Version][s-npm-version-image]][s-npm-url]
6[![NPM Install Size][s-npm-install-size-image]][s-npm-install-size-url]
7[![NPM Downloads][s-npm-downloads-image]][s-npm-downloads-url]
8
9authpal-client
10
11[![NPM Version][c-npm-version-image]][c-npm-url]
12[![NPM Install Size][c-npm-install-size-image]][c-npm-install-size-url]
13[![NPM Downloads][c-npm-downloads-image]][c-npm-downloads-url]
14
15<br/>
16
17A node package to handle user authentication and authorization securely on both client and server.
18
19**Built on top of express, passport and jwt.**
20
21Its goal is to be simple to use yet up to security standards. And be reusable across different apps so you don't have to rewrite the same thing every time you build a web app.
22
23It uses the **accessToken & refreshToken** combo.
24The latter is stored in cookies and the former should be stored in memory _(let accessToken, not localStorage.accessToken)_.
25
26## Server
27
28For quick setup follow [1️⃣](#1️⃣-setup) and [2️⃣](#2️⃣-configs-concretely).
29Your greens 🥦 are pretty good to have but you don't necessarily have to read.
30
31## Client
32
33Setup your project following [3️⃣](#3️⃣-setup) then [4️⃣](#4️⃣-configs) explains the configuration for the client side better.
34
35</br>
36</br>
37
38# Server Side Usage
39
40## 1️⃣ Setup
41
42</br>
43
44**Prerequisites:**
45
46Your express app should use the following
47
48```typescript
49import * as bodyParser from 'body-parser'
50import * as cookieParser from 'cookie-parser'
51
52//...
53
54app.use(bodyParser.json())
55app.use(cookieParser())
56
57/*
58 cors should also be set.
59 those are required for secure httpOnly cookies by browsers
60*/
61```
62
63</br>
64</br>
65
66Install the package:
67
68```bash
69npm install authpal
70```
71
72Import the package and istantiate it:
73
74```typescript
75import { Authpal } from 'authpal'
76
77//...
78
79let authpal = new Authpal({
80 //AuthpalConfigs are mandatory, read the 'Configs (concretely)' paragraph below
81})
82
83//...
84```
85
86Create your routes using the prebuilt middlewares:
87
88```typescript
89//retrieve accessToken and set-cookie refreshToken
90app.post('/login', authpal.loginMiddleWare) //no need to setup response
91
92//generate a new accessToken via refreshToken cookie
93app.get('/resume', authpal.resumeMiddleware) //no need to setup response
94
95//verify headers have 'Bearer <accessToken>'
96app.get('/secure', authpal.authorizationMiddleware, (req, res) => {
97 let user = req.user
98
99 //DO YOUR THINGS HERE
100
101 res.sendStatus(200)
102})
103```
104
105</br>
106
107## 🥦 Configs
108
109<details>
110
111<summary>The following section describes the config object. Click to expand.</summary>
112
113---
114
115<br/>
116
117The way the interface is setup requires you to define callbacks to:
118
119- find your user by username/id/refreshToken
120- verify password
121- store refreshToken
122
123This allows you to handle your user data however you prefer.
124
125The configs type looks like this:
126
127```typescript
128{
129 jwtSecret: string //A secret used to encrypt the JWTs (usually in process.env.JWT_SECRET)
130
131 /*
132 By default authpal will look in the /login request body for 'username' and 'password'.
133 These can be changed if you'd rather call them something else
134 */
135 usernameField?: string //Overrides 'username'
136 passwordField?: string //Overrides 'password'
137
138 refreshTokenExpiration?: number //How many seconds before refresh token expires (default 14 days)
139
140
141 //A callback that must return the User Payload based on the username
142 findUserByUsernameCallback(
143 username: string
144 ): Promise<AuthpalJWTPayload | null> | AuthpalJWTPayload | null
145
146 //A callback that must return the User Payload based on the user ID
147 findUserByIDCallback(
148 userid: string | number
149 ): Promise<AuthpalJWTPayload | null> | AuthpalJWTPayload | null
150
151 //A callback that must return the User Payload based on the token
152 findUserByRefreshToken(
153 refreshToken: string
154 ): Promise<AuthpalJWTPayload | null> | AuthpalJWTPayload | null
155
156 //A callback that must return a boolean after verifying that password matches the user
157 verifyPasswordCallback(
158 username: string,
159 password: string
160 ): Promise<boolean> | boolean
161
162 /*
163 A callback that returns the refresh token object as well as the associated User Payload.
164 Use this to store the token in your database.
165 */
166 tokenRefreshedCallback(
167 jwtPayload: AuthpalJWTPayload,
168 token: RefreshToken
169 ): Promise<void> | void
170
171 /*
172 A callback that returns the refresh token object as well as the associated User Payload.
173 Use this to delete the token from your database when user logs out.
174 */
175 tokenDeletedCallback(
176 jwtPayload: AuthpalJWTPayload,
177 token: RefreshToken
178 ): Promise<void> | void
179}
180```
181
182</details>
183
184</br>
185
186</br>
187
188## 🥦 Understand the User Payload (AuthpalJWTPayload)
189
190<details>
191
192<summary>The following section describes the Payload object. Click to expand. </summary>
193
194---
195
196<br/>
197
198`AuthpalJWTPayload` is defined as
199
200```typescript
201{
202 userid?: string | number
203}
204```
205
206This is the object that will be passed around the middlewares and put into a JWT on the client's cookies.
207
208If you don't understand what this is, your best bet is to just leave it as is, but this is passed as a generic and can therefore be extended.
209
210For example if you require to send some more data you can do it this way:
211
212```typescript
213interface MyCustomPayload extends AuthpalJWTPayload {
214 mayTheForce: 'Be with you. No Kathleen, not you...'
215}
216
217let authpal = new Authpal<MyCustomPayload>({
218 //configs
219})
220
221//at this point if you need to extract it out of a secured route you can access
222app.get('/secure', authpal.authorizationMiddleware, (req, res) => {
223 let user = req.user.MayTheForce //'Be with you. No Kathleen, not you...'
224 res.sendStatus(200)
225})
226```
227
228</details>
229
230</br>
231
232## 2️⃣ Configs (concretely)
233
234</br>
235
236If you skipped the previous two paragraphs, it doesn't really matter, all you need to know is that you need to setup at least the basic configs in a similar fashion
237
238```typescript
239let authpal = new Authpal({
240 jwtSecret: 'myJWTsecret', //please don't hardcode it but process.env.JWT_SECRET or something,
241
242 //These examples are with mongo & mongoose but obviously you need to implement your own fetch callbacks
243 findUserByUsernameCallback: async (username) => {
244 return await UsersModel.findOne({ username })
245 },
246 findUserByIDCallback: async (userid) => {
247 return await UsersModel.findOne({ _id: userid })
248 },
249 findUserByRefreshToken: async (token) => {
250 let session = await SessionsModel.findOne({ token }) //You can save the tokens wherever you want, even straight up in the users documents.
251 return {
252 userid: session.user,
253 }
254 },
255 tokenRefreshedCallback: async (jwtPayload, token) => {
256 UsersModel.findOne({ _id: jwtPayload.userid }).then((user) => {
257 //Delete existing or update them to your discretion
258 await SessionsModel.create({
259 user: jwtPayload.userid,
260 token: token.token,
261 expiration: token.expiration,
262 })
263 })
264 },
265 tokenDeletedCallback: async (jwtPayload, token) => {
266 UsersModel.findOne({ _id: jwtPayload.userid }).then((user) => {
267 //Delete the token on logout
268 await SessionsModel.deleteMany({
269 user: jwtPayload.userid,
270 token: token.token,
271 })
272 })
273 },
274
275 //Example with bcrypt but you can implement your own
276 verifyPasswordCallback: (username, password) => {
277 let user = await UsersModel.findOne({ username })
278 return bcrypt.compareSync(password, user.hash)
279 },
280})
281```
282
283</br>
284</br>
285</br>
286
287# Client Side Usage
288
289## 3️⃣ Setup
290
291</br>
292
293Install the package:
294
295```bash
296npm install authpal-client
297```
298
299Import the package and istantiate it:
300
301```typescript
302import { AuthpalClient } from 'authpal'
303
304//...
305
306let authpalClient = new AuthpalClient({
307 //AuthpalClientConfigs are mandatory, read the 'Configs' paragraph below
308})
309
310//...
311```
312
313Here's how to use the library
314
315```typescript
316/*
317 RESUME SESSION
318
319 As soon as your application start attempt to resume to revalidate the refresh cookie.
320 If it exists and the server validates it will receive a new access token and be logged in again.
321*/
322authpalClient.attemptResume()
323
324/*
325 LOGIN
326
327 This method takes a credentials object as a parameter.
328 These have to match what you selected on the server side as overrides.
329 You you didn't change anything they'll be 'username' and 'password'.
330*/
331authpalClient.login({
332 username: 'myusername',
333 password: 'asupersecretpassword',
334})
335
336/*
337 LOGOUT
338*/
339authpalClient.logout()
340```
341
342All of these methods are async Promises and can be awaited for.
343
344The best method to catch the events is through the emitter that you can pass via the ClientConfigs:
345
346```typescript
347userChangesEmitter.subscribe((changes) => {
348 //This fires with every event and change in login status.
349})
350
351/*
352 resomeDoneEmitter is a Subject<void> (from 'rxjs').
353 This is marked as .complete() whenever the attemptResume() is done.
354
355 This is particulary useful when you need to wait until the resume
356 process is over before doing other things like rendering or requesting data
357 */
358await resumeDoneEmitter.toPromise() //Will only continue when is alredy or gets completed
359
360/*
361 Sometimes you wanna do more stuff before the resume process is over.
362 You can provide a middleware function that gets fired right before completing succcessfully.
363 */
364{
365 //... your client configs
366 resumeDoneMiddleware: async (changes) => {
367 //User requests are now authenticated
368 //Do whatever you need to do (ask for user data or ...)
369 }
370}
371```
372
373Once you're authenticated, you can pass the authorization token to your request library of choice like so:
374
375```typescript
376//This example is with axios but it should work with any library
377
378axios
379 .get({
380 method: 'get',
381 url: 'https://example.com/api/v1/secure/private/get-out/please-no',
382 headers: {
383 //Your other custom headers go here
384
385 //Add Auth headers to the others
386 ...this.authPalClient.getAuthorizationHeader(),
387 },
388 })
389 .then(({ data }) => {
390 console.log(data)
391 })
392```
393
394Whenever you receive a userChangesEvent it's defined like so:
395
396```typescript
397/*
398 The changes fired in your
399 userChangesEmitter.subscribe((changes) => {})
400*/
401{
402 type: string //this can be 'login', 'resume' or 'logout'
403 authenticated: boolean //is user authenticated after this event?
404}
405```
406
407## 4️⃣ Configs
408
409The AuthpalClientConfigs object is defined this way:
410
411```typescript
412/*
413 You wanna keep an outside reference to these so you can subscribe
414 and listen to events, or await for the resume process to be done
415*/
416let userChangesEmitter = new UserChangesEmitter()
417let resumeDoneEmitter = new Subject()
418
419let authpalClient = new AuthpalClient({
420 //The POST endpoint for logging in on your server
421 loginPostUrl: 'https://example.com/api/v1/login',
422 //The GET endpoint for resuming the session in on your server
423 loginPostUrl: 'https://example.com/api/v1/login',
424 //The GET endpoint for logging out on your server
425 //If you don't call this the session will not be closed, hence resumed on page refresh
426 logoutGetURL: 'https://example.com/api/v1/logout',
427
428 //The custom subject that emits changes to the user (As defined above)
429 userChangesEmitter: userChangesEmitter,
430
431 //A Subject<void> that gets completed when the resume attempt is over (See 3️⃣)
432 resumeDoneEmitter: resumeDoneEmitter,
433
434 //(optional) A middleware callback to call right before a resume request succeeds
435 resumeDoneMiddleware: async (changes) => {
436 //Do your things... You're already authenticated at this point
437 },
438})
439```
440
441[s-npm-version-image]: https://badgen.net/npm/v/authpal
442[s-npm-url]: https://npmjs.org/package/authpal
443[s-npm-install-size-image]: https://badgen.net/packagephobia/install/authpal
444[s-npm-install-size-url]: https://packagephobia.com/result?p=authpal
445[s-npm-downloads-image]: https://badgen.net/npm/dm/authpal
446[s-npm-downloads-url]: https://npmcharts.com/compare/authpal?minimal=true
447[c-npm-version-image]: https://badgen.net/npm/v/authpal-client
448[c-npm-url]: https://npmjs.org/package/authpal-client
449[c-npm-install-size-image]: https://badgen.net/packagephobia/install/authpal-client
450[c-npm-install-size-url]: https://packagephobia.com/result?p=authpal-client
451[c-npm-downloads-image]: https://badgen.net/npm/dm/authpal-client
452[c-npm-downloads-url]: https://npmcharts.com/compare/authpal-client?minimal=true