-
Notifications
You must be signed in to change notification settings - Fork 70
212 password flow #281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
212 password flow #281
Changes from 10 commits
062080e
6e6106d
4b2c0cd
92bb68f
f6a9636
ba0358b
0249b2d
a8312ed
45e0118
8fbdc12
6b781d7
76eb40f
84a3057
3a42900
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| /* @flow */ | ||
| import type { | ||
| MiddlewareRequest, | ||
| MiddlewareResponse, | ||
| Next, | ||
| Task, | ||
| } from 'types/sdk' | ||
|
|
||
| /* global fetch */ | ||
| import 'isomorphic-fetch' | ||
|
|
||
| type RequestState = boolean | ||
| type TokenStore = { | ||
| token: string; | ||
| expirationTime: number; | ||
| } | ||
| type AuthMiddlewareBaseOptions = { | ||
| request: MiddlewareRequest; | ||
| response: MiddlewareResponse; | ||
| url: string; | ||
| body: string; | ||
| basicAuth: string; | ||
| pendingTasks: Array<Task>; | ||
| requestState: { | ||
| get: () => RequestState; | ||
| set: (requestState: RequestState) => RequestState; | ||
| }; | ||
| tokenCache: { | ||
| get: () => TokenStore; | ||
| set: (cache: TokenStore) => TokenStore; | ||
| } | ||
| } | ||
|
|
||
| export default function authMiddlewareBase ({ | ||
| request, | ||
| response, | ||
| url, | ||
| basicAuth, | ||
| body, | ||
| pendingTasks, | ||
| requestState, | ||
| tokenCache, | ||
| }: AuthMiddlewareBaseOptions, | ||
| next: Next, | ||
| ) { | ||
| // Check if there is already a `Authorization` header in the request. | ||
| // If so, then go directly to the next middleware. | ||
| if ( | ||
| (request.headers && request.headers['authorization']) || | ||
| (request.headers && request.headers['Authorization']) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why are you covering both?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because it's can be an issue that occurred from the http module used when fetching OAuth token from the API |
||
| ) { | ||
| next(request, response) | ||
| return | ||
| } | ||
|
|
||
| // If there was a token in the tokenCache, and it's not expired, append | ||
| // the token in the `Authorization` header. | ||
| const tokenObj = tokenCache.get() | ||
| if (tokenObj && tokenObj.token && Date.now() < tokenObj.expirationTime) { | ||
| const requestWithAuth = mergeAuthHeader(tokenObj.token, request) | ||
| next(requestWithAuth, response) | ||
| return | ||
| } | ||
| // Token is not present or is invalid. Request a new token... | ||
|
|
||
| // Keep pending tasks until a token is fetched | ||
| pendingTasks.push({ request, response }) | ||
|
|
||
| // If a token is currently being fetched, just wait ;) | ||
| if (requestState.get()) return | ||
|
|
||
| // Mark that a token is being fetched | ||
| requestState.set(true) | ||
|
|
||
| fetch( | ||
| url, | ||
| { | ||
| method: 'POST', | ||
| headers: { | ||
| Authorization: `Basic ${basicAuth}`, | ||
| 'Content-Length': Buffer.byteLength(body).toString(), | ||
| 'Content-Type': 'application/x-www-form-urlencoded', | ||
| }, | ||
| body, | ||
| }, | ||
| ) | ||
| .then((res: Response): Promise<*> => { | ||
| if (res.ok) | ||
| return res.json() | ||
| .then((result: Object) => { | ||
| const token = result.access_token | ||
| const expiresIn = result.expires_in | ||
| const expirationTime = calculateExpirationTime(expiresIn) | ||
|
|
||
| // Cache new token | ||
| tokenCache.set({ token, expirationTime }) | ||
|
|
||
| // Dispatch all pending requests | ||
| requestState.set(false) | ||
|
|
||
| // Freeze and copy pending queue, reset original one for accepting | ||
| // new pending tasks | ||
| const executionQueue = pendingTasks.slice() | ||
| // eslint-disable-next-line no-param-reassign | ||
| pendingTasks = [] | ||
| executionQueue.forEach((task: Task) => { | ||
| // Assign the new token in the request header | ||
| const requestWithAuth = mergeAuthHeader(token, task.request) | ||
| // console.log('test', cache, pendingTasks) | ||
| next(requestWithAuth, task.response) | ||
| }) | ||
| }) | ||
|
|
||
| // Handle error response | ||
| return res.text() | ||
| .then((text: any) => { | ||
| let parsed | ||
| try { | ||
| parsed = JSON.parse(text) | ||
| } catch (error) { | ||
| /* noop */ | ||
| } | ||
| const error: Object = new Error(parsed ? parsed.message : text) | ||
| if (parsed) error.body = parsed | ||
| response.reject(error) | ||
| }) | ||
| }) | ||
| .catch((error: Error) => { | ||
| response.reject(error) | ||
| }) | ||
| } | ||
|
|
||
| function mergeAuthHeader ( | ||
| token: string, | ||
| req: MiddlewareRequest, | ||
| ): MiddlewareRequest { | ||
| return { | ||
| ...req, | ||
| headers: { | ||
| ...req.headers, | ||
| Authorization: `Bearer ${token}`, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| function calculateExpirationTime (expiresIn: number): number { | ||
| return ( | ||
| Date.now() + | ||
| (expiresIn * 1000) | ||
| ) - ( | ||
| // Add a gap of 2 hours before expiration time. | ||
| 2 * 60 * 60 * 1000 | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,8 @@ | ||
| /* @flow */ | ||
| import type { AuthMiddlewareOptions } from 'types/sdk' | ||
| import type { | ||
| AuthMiddlewareOptions, | ||
| PasswordAuthMiddlewareOptions, | ||
| } from 'types/sdk' | ||
| import * as authScopes from './scopes' | ||
|
|
||
| type BuiltRequestParams = { | ||
|
|
@@ -43,8 +46,47 @@ export function buildRequestForClientCredentialsFlow ( | |
| return { basicAuth, url, body } | ||
| } | ||
|
|
||
| export function buildRequestForPasswordFlow () { | ||
| // TODO | ||
| export function buildRequestForPasswordFlow ( | ||
| options: PasswordAuthMiddlewareOptions, | ||
| ): BuiltRequestParams { | ||
| if (!options) | ||
| throw new Error('Missing required options') | ||
|
|
||
| if (!options.host) | ||
| throw new Error('Missing required option (host)') | ||
|
|
||
| if (!options.projectKey) | ||
| throw new Error('Missing required option (projectKey)') | ||
|
|
||
| if (!options.credentials) | ||
| throw new Error('Missing required option (credentials)') | ||
|
|
||
| const { | ||
| clientId, | ||
| clientSecret, | ||
| user, | ||
| } = options.credentials | ||
| const pKey = options.projectKey | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shorter var names to avoid eslint error and unnecessary multiline I think it's a good trade off 🤷♂️ |
||
| if (!(clientId && clientSecret && user)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For better handling, this could be checked one after the other
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you explain what you mean by "better handling"?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Better usability"
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, all is at the same level else we can as well begin to validate each field, and that won't make too much sense. That's why I am validating That's my thinking |
||
| throw new Error( | ||
| 'Missing required credentials (clientId, clientSecret, user)', | ||
| ) | ||
| const { username, password } = user | ||
| if (!(username && password)) | ||
| throw new Error('Missing required user credentials (username, password)') | ||
|
|
||
| const defaultScope = `${authScopes.MANAGE_PROJECT}:${pKey}` | ||
| const scope = (options.scopes || [defaultScope]).join(' ') | ||
|
|
||
| const basicAuth = new Buffer(`${clientId}:${clientSecret}`).toString('base64') | ||
| // This is mostly useful for internal testing purposes to be able to check | ||
| // other oauth endpoints. | ||
| const oauthUri = options.oauthUri || `/oauth/${pKey}/token/customers/token` | ||
| const url = options.host.replace(/\/$/, '') + oauthUri | ||
| // eslint-disable-next-line max-len | ||
| const body = `grant_type=password&scope=${scope}&username=${username}&password=${password}` | ||
|
|
||
| return { basicAuth, url, body } | ||
| } | ||
|
|
||
| export function buildRequestForRefreshTokenFlow () { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it not be better to add the type-declarations in the
typesfolder and import?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's only used here, hence the reason but yeah can be refactored to the types folder