-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add oauth authentication using
useRedirectUI
custom configura…
…tion (#105) * * creating new parameter `useRedirectUI` that enables hosting an endpoint for using the standard OAuth 2.0 Authorization Code Grant Flow. * Updating swagger plugin to include middleware that puts scopes next to the authorization widget for each endpoint * PR fixes * copy pasta error
- Loading branch information
1 parent
b0a0042
commit 0bb8432
Showing
8 changed files
with
241 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
exports.handler = async () => { | ||
return { | ||
statusCode: 200, | ||
body: redirectUI, | ||
headers: { | ||
'content-type': 'text/html', | ||
}, | ||
}; | ||
}; | ||
|
||
// copied from view-source:https://unpkg.com/[email protected]/oauth2-redirect.html | ||
const redirectUI = `<!doctype html> | ||
<html lang="en-US"> | ||
<head> | ||
<title>Swagger UI: OAuth2 Redirect</title> | ||
</head> | ||
<body> | ||
<script> | ||
'use strict'; | ||
function run () { | ||
var oauth2 = window.opener.swaggerUIRedirectOauth2; | ||
var sentState = oauth2.state; | ||
var redirectUrl = oauth2.redirectUrl; | ||
var isValid, qp, arr; | ||
if (/code|token|error/.test(window.location.hash)) { | ||
qp = window.location.hash.substring(1); | ||
} else { | ||
qp = location.search.substring(1); | ||
} | ||
arr = qp.split("&"); | ||
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';}); | ||
qp = qp ? JSON.parse('{' + arr.join() + '}', | ||
function (key, value) { | ||
return key === "" ? value : decodeURIComponent(value); | ||
} | ||
) : {}; | ||
isValid = qp.state === sentState; | ||
if (( | ||
oauth2.auth.schema.get("flow") === "accessCode" || | ||
oauth2.auth.schema.get("flow") === "authorizationCode" || | ||
oauth2.auth.schema.get("flow") === "authorization_code" | ||
) && !oauth2.auth.code) { | ||
if (!isValid) { | ||
oauth2.errCb({ | ||
authId: oauth2.auth.name, | ||
source: "auth", | ||
level: "warning", | ||
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server" | ||
}); | ||
} | ||
if (qp.code) { | ||
delete oauth2.state; | ||
oauth2.auth.code = qp.code; | ||
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl}); | ||
} else { | ||
let oauthErrorMsg; | ||
if (qp.error) { | ||
oauthErrorMsg = "["+qp.error+"]: " + | ||
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") + | ||
(qp.error_uri ? "More info: "+qp.error_uri : ""); | ||
} | ||
oauth2.errCb({ | ||
authId: oauth2.auth.name, | ||
source: "auth", | ||
level: "error", | ||
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server" | ||
}); | ||
} | ||
} else { | ||
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl}); | ||
} | ||
window.close(); | ||
} | ||
window.addEventListener('DOMContentLoaded', function () { | ||
run(); | ||
}); | ||
</script> | ||
</body> | ||
</html>`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,15 +18,50 @@ const swaggerUI = `<!DOCTYPE html> | |
rel="stylesheet" | ||
href="https://unpkg.com/[email protected]/swagger-ui.css" | ||
/> | ||
<script src="https://unpkg.com/react@15/dist/react.min.js"></script> | ||
<script src="https://unpkg.com/[email protected]/swagger-ui-bundle.js"></script> | ||
<script src="https://unpkg.com/[email protected]/swagger-ui-standalone-preset.js"></script> | ||
<script defer> | ||
window.onload = () => { | ||
const h = React.createElement | ||
const ui = SwaggerUIBundle({ | ||
url: window.location.href + '.json', | ||
dom_id: '#swagger-ui', | ||
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset], | ||
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset, | ||
system => { | ||
// Variable to capture the security prop of OperationSummary | ||
// then pass it to authorizeOperationBtn | ||
let currentSecurity | ||
return { | ||
wrapComponents: { | ||
// Wrap OperationSummary component to get its prop | ||
OperationSummary: Original => props => { | ||
const security = props.operationProps.get('security') | ||
currentSecurity = security.toJS() | ||
return h(Original, props) | ||
}, | ||
// Wrap the padlock button to show the | ||
// scopes required for current operation | ||
authorizeOperationBtn: Original => | ||
function (props) { | ||
return h('div', {}, [ | ||
...(currentSecurity || []).map(scheme => { | ||
const schemeName = Object.keys(scheme)[0] | ||
if (!scheme[schemeName].length) return null | ||
const scopes = scheme[schemeName].flatMap(scope => [ | ||
h('code', null, scope), | ||
', ', | ||
]) | ||
scopes.pop() | ||
return h('span', null, scopes) | ||
}), | ||
h(Original, props), | ||
]) | ||
}, | ||
}, | ||
} | ||
}], | ||
layout: 'StandaloneLayout', | ||
}); | ||
window.ui = ui; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import swaggerFunctions from '../src/resources/functions'; | ||
import { AutoSwaggerCustomConfig, CustomServerless } from '../src/types/serverless-plugin.types'; | ||
|
||
const defaultServiceDetails: CustomServerless = { | ||
configSchemaHandler: { | ||
defineCustomProperties: () => undefined, | ||
defineFunctionEvent: () => undefined, | ||
defineFunctionEventProperties: () => undefined, | ||
defineFunctionProperties: () => undefined, | ||
defineProvider: () => undefined, | ||
defineTopLevelProperty: () => undefined, | ||
}, | ||
configurationInput: {}, | ||
service: { | ||
service: '', | ||
provider: { | ||
name: 'aws', | ||
runtime: undefined, | ||
stage: '', | ||
region: undefined, | ||
profile: '', | ||
environment: {}, | ||
}, | ||
plugins: [], | ||
functions: { | ||
mocked: { | ||
handler: 'mocked.handler', | ||
}, | ||
}, | ||
custom: { | ||
autoswagger: {}, | ||
}, | ||
}, | ||
}; | ||
|
||
type GetCustomServerlessConfigParams = { | ||
autoswaggerOptions?: AutoSwaggerCustomConfig; | ||
}; | ||
|
||
const getCustomServerlessConfig = ({ autoswaggerOptions }: GetCustomServerlessConfigParams = {}): CustomServerless => ({ | ||
...defaultServiceDetails, | ||
service: { | ||
...defaultServiceDetails.service, | ||
custom: { | ||
...defaultServiceDetails.service.custom, | ||
autoswagger: { | ||
...defaultServiceDetails.service.custom?.autoswagger, | ||
...autoswaggerOptions, | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
describe('swaggerFunctions tests', () => { | ||
it('includes swaggerRedirectURI if useRedirectUI is set to true', () => { | ||
const serviceDetails = getCustomServerlessConfig({ | ||
autoswaggerOptions: { | ||
useRedirectUI: true, | ||
}, | ||
}); | ||
const result = swaggerFunctions(serviceDetails); | ||
expect(Object.keys(result)).toContain('swaggerRedirectURI'); | ||
}); | ||
|
||
it('does not includes swaggerRedirectURI if useRedirectUI is set to false', () => { | ||
const serviceDetails = getCustomServerlessConfig({ | ||
autoswaggerOptions: { | ||
useRedirectUI: false, | ||
}, | ||
}); | ||
const result = swaggerFunctions(serviceDetails); | ||
expect(Object.keys(result)).not.toContain('swaggerRedirectURI'); | ||
}); | ||
|
||
it('does not includes swaggerRedirectURI if useRedirectUI is not set', () => { | ||
const serviceDetails = getCustomServerlessConfig(); | ||
const result = swaggerFunctions(serviceDetails); | ||
expect(Object.keys(result)).not.toContain('swaggerRedirectURI'); | ||
}); | ||
}); |