Browse Source

improve: OAuth2 UI and test suite (via #5066)

* create `features` folder

* add base oauth2 server

* continue implementing OAuth tests

* WIP

* add password flow tests

* modify Password flow credential types

* remove query string credential type

* add test case for Authorization flow

* add specific Authorization value for Password flow test

* WIP

* fix linter issues
bubble
kyle 5 years ago
committed by GitHub
parent
commit
a5568f9e16
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 504 additions and 55 deletions
  1. +68
    -30
      package-lock.json
  2. +4
    -0
      package.json
  3. +6
    -7
      src/core/components/auth/oauth2.jsx
  4. +1
    -1
      src/core/components/live-response.jsx
  5. +12
    -17
      src/core/plugins/auth/actions.js
  6. +0
    -0
      test/e2e-cypress/.eslintrc
  7. +50
    -0
      test/e2e-cypress/helpers/oauth2-server/index.js
  8. +141
    -0
      test/e2e-cypress/helpers/oauth2-server/model.js
  9. +36
    -0
      test/e2e-cypress/helpers/oauth2-server/swagger.yaml
  10. +2
    -0
      test/e2e-cypress/plugins/index.js
  11. +7
    -0
      test/e2e-cypress/support/index.js
  12. +0
    -0
      test/e2e-cypress/tests/features/deep-linking.js
  13. +55
    -0
      test/e2e-cypress/tests/features/oauth2-flows/application.js
  14. +122
    -0
      test/e2e-cypress/tests/features/oauth2-flows/password.js

+ 68
- 30
package-lock.json View File

@@ -2341,28 +2341,49 @@
"dev": true
},
"body-parser": {
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz",
"integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=",
"version": "1.18.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
"integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
"dev": true,
"requires": {
"bytes": "3.0.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.1",
"http-errors": "~1.6.2",
"iconv-lite": "0.4.19",
"depd": "~1.1.2",
"http-errors": "~1.6.3",
"iconv-lite": "0.4.23",
"on-finished": "~2.3.0",
"qs": "6.5.1",
"raw-body": "2.3.2",
"type-is": "~1.6.15"
"qs": "6.5.2",
"raw-body": "2.3.3",
"type-is": "~1.6.16"
},
"dependencies": {
"iconv-lite": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
"integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==",
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
"dev": true
},
"raw-body": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
"integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
"dev": true,
"requires": {
"bytes": "3.0.0",
"http-errors": "1.6.3",
"iconv-lite": "0.4.23",
"unpipe": "1.0.0"
}
}
}
},
@@ -6421,14 +6442,14 @@
}
},
"express": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz",
"integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=",
"version": "4.16.4",
"resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
"integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
"dev": true,
"requires": {
"accepts": "~1.3.5",
"array-flatten": "1.1.1",
"body-parser": "1.18.2",
"body-parser": "1.18.3",
"content-disposition": "0.5.2",
"content-type": "~1.0.4",
"cookie": "0.3.1",
@@ -6445,10 +6466,10 @@
"on-finished": "~2.3.0",
"parseurl": "~1.3.2",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.3",
"qs": "6.5.1",
"proxy-addr": "~2.0.4",
"qs": "6.5.2",
"range-parser": "~1.2.0",
"safe-buffer": "5.1.1",
"safe-buffer": "5.1.2",
"send": "0.16.2",
"serve-static": "1.13.2",
"setprototypeof": "1.1.0",
@@ -6464,10 +6485,10 @@
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
"dev": true
},
"safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
"dev": true
},
"statuses": {
@@ -9397,9 +9418,9 @@
"dev": true
},
"ipaddr.js": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz",
"integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
"integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=",
"dev": true
},
"is-absolute-url": {
@@ -15478,6 +15499,23 @@
"integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=",
"dev": true
},
"oauth2-server": {
"version": "2.4.1",
"resolved": "http://registry.npmjs.org/oauth2-server/-/oauth2-server-2.4.1.tgz",
"integrity": "sha1-2m3QVMAh7JwpQ59dGijeY9ArcWw=",
"dev": true,
"requires": {
"basic-auth": "~0.0.1"
},
"dependencies": {
"basic-auth": {
"version": "0.0.1",
"resolved": "http://registry.npmjs.org/basic-auth/-/basic-auth-0.0.1.tgz",
"integrity": "sha1-Md22WEP2w1xv6nvrRqmHy4zhiSQ=",
"dev": true
}
}
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -18364,13 +18402,13 @@
}
},
"proxy-addr": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz",
"integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
"integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==",
"dev": true,
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.6.0"
"ipaddr.js": "1.8.0"
}
},
"proxy-agent": {


+ 4
- 0
package.json View File

@@ -95,9 +95,11 @@
"babel-preset-react": "^6.23.0",
"babel-preset-stage-0": "^6.22.0",
"babel-runtime": "^6.23.0",
"body-parser": "^1.18.3",
"bundlesize": "^0.17.0",
"chromedriver": "^2.38.3",
"copy-webpack-plugin": "^4.0.1",
"cors": "^2.8.4",
"css-loader": "^0.28.11",
"cypress": "^3.1.0",
"dedent": "^0.7.0",
@@ -108,6 +110,7 @@
"eslint-plugin-mocha": "^4.11.0",
"eslint-plugin-react": "^7.10.0",
"expect": "^1.20.2",
"express": "^4.16.4",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.11",
"git-describe": "^4.0.1",
@@ -124,6 +127,7 @@
"npm-run-all": "^4.1.2",
"null-loader": "0.1.1",
"nyc": "^11.3.0",
"oauth2-server": "^2.4.1",
"open": "0.0.5",
"postcss-loader": "^2.1.5",
"raw-loader": "0.5.1",


+ 6
- 7
src/core/components/auth/oauth2.jsx View File

@@ -24,7 +24,7 @@ export default class Oauth2 extends React.Component {
let username = auth && auth.get("username") || ""
let clientId = auth && auth.get("clientId") || authConfigs.clientId || ""
let clientSecret = auth && auth.get("clientSecret") || authConfigs.clientSecret || ""
let passwordType = auth && auth.get("passwordType") || "request-body"
let passwordType = auth && auth.get("passwordType") || "basic"

this.state = {
appName: authConfigs.appName,
@@ -150,14 +150,13 @@ export default class Oauth2 extends React.Component {
}
</Row>
<Row>
<label htmlFor="password_type">type:</label>
<label htmlFor="password_type">Client credentials location:</label>
{
isAuthorized ? <code> { this.state.passwordType } </code>
: <Col tablet={10} desktop={10}>
<select id="password_type" data-name="passwordType" onChange={ this.onInputChange }>
<option value="basic">Authorization header</option>
<option value="request-body">Request body</option>
<option value="basic">Basic auth</option>
<option value="query">Query parameters</option>
</select>
</Col>
}
@@ -165,7 +164,7 @@ export default class Oauth2 extends React.Component {
</Row>
}
{
( flow === APPLICATION || flow === IMPLICIT || flow === ACCESS_CODE || ( flow === PASSWORD && this.state.passwordType!== "basic") ) &&
( flow === APPLICATION || flow === IMPLICIT || flow === ACCESS_CODE || flow === PASSWORD ) &&
( !isAuthorized || isAuthorized && this.state.clientId) && <Row>
<label htmlFor="client_id">client_id:</label>
{
@@ -183,7 +182,7 @@ export default class Oauth2 extends React.Component {
}

{
( flow === APPLICATION || flow === ACCESS_CODE || ( flow === PASSWORD && this.state.passwordType!== "basic") ) && <Row>
( (flow === APPLICATION || flow === ACCESS_CODE || flow === PASSWORD) && <Row>
<label htmlFor="client_secret">client_secret:</label>
{
isAuthorized ? <code> ****** </code>
@@ -197,7 +196,7 @@ export default class Oauth2 extends React.Component {
}

</Row>
}
)}

{
!isAuthorized && scopes && scopes.size ? <div className="scopes">


+ 1
- 1
src/core/components/live-response.jsx View File

@@ -80,7 +80,7 @@ export default class LiveResponse extends React.Component {
</div>
}
<h4>Server response</h4>
<table className="responses-table">
<table className="responses-table live-responses-table">
<thead>
<tr className="responses-header">
<td className="col col_header response-col_status">Code</td>


+ 12
- 17
src/core/plugins/auth/actions.js View File

@@ -74,28 +74,23 @@ export const authorizePassword = ( auth ) => ( { authActions } ) => {
let { schema, name, username, password, passwordType, clientId, clientSecret } = auth
let form = {
grant_type: "password",
scope: auth.scopes.join(scopeSeparator)
scope: auth.scopes.join(scopeSeparator),
username,
password
}
let query = {}
let headers = {}

if ( passwordType === "basic") {
headers.Authorization = "Basic " + btoa(username + ":" + password)
} else {
Object.assign(form, {username}, {password})

switch ( passwordType ) {
case "query":
setClientIdAndSecret(query, clientId, clientSecret)
break
switch (passwordType) {
case "request-body":
setClientIdAndSecret(form, clientId, clientSecret)
break

case "request-body":
setClientIdAndSecret(form, clientId, clientSecret)
break

default:
headers.Authorization = "Basic " + btoa(clientId + ":" + clientSecret)
}
case "basic":
headers.Authorization = "Basic " + btoa(clientId + ":" + clientSecret)
break
default:
console.warn(`Warning: invalid passwordType ${passwordType} was passed, not including client id and secret`)
}

return authActions.authorizeRequest({ body: buildFormData(form), url: schema.get("tokenUrl"), name, headers, query, auth})


test/e2e-cypress/tests/.eslintrc → test/e2e-cypress/.eslintrc View File


+ 50
- 0
test/e2e-cypress/helpers/oauth2-server/index.js View File

@@ -0,0 +1,50 @@
// from https://github.com/pedroetb/node-oauth2-server-example

var Http = require("http")
var path = require("path")
var express = require("express")
var bodyParser = require("body-parser")
var oauthserver = require("oauth2-server")
var cors = require("cors")

var app = express()

app.use(cors())

app.use(bodyParser.urlencoded({ extended: true }))

app.use(bodyParser.json())

app.oauth = oauthserver({
model: require("./model.js"),
grants: ["password", "client_credentials", "implicit"],
debug: true
})

app.all("/oauth/token", app.oauth.grant())

app.get("/swagger.yaml", function (req, res) {
res.sendFile(path.join(__dirname, "swagger.yaml"))
})

app.get("*", app.oauth.authorise(), function (req, res) {
res.send("Secret secrets are no fun, secret secrets hurt someone.")
})

app.use(app.oauth.errorHandler())

function startServer() {
var httpServer = Http.createServer(app)
httpServer.listen("3231")

return function stopServer() {
httpServer.close()
}
}

module.exports = startServer

if (require.main === module) {
// for debugging
startServer()
}

+ 141
- 0
test/e2e-cypress/helpers/oauth2-server/model.js View File

@@ -0,0 +1,141 @@
// from https://github.com/pedroetb/node-oauth2-server-example

var config = {
clients: [{
clientId: "application",
clientSecret: "secret"
}],
confidentialClients: [{
clientId: "confidentialApplication",
clientSecret: "topSecret"
}],
tokens: [],
users: [{
id: "123",
username: "swagger",
password: "password"
}]
}

/**
* Dump the memory storage content (for debug).
*/

var dump = function () {

console.log("clients", config.clients)
console.log("confidentialClients", config.confidentialClients)
console.log("tokens", config.tokens)
console.log("users", config.users)
}

/*
* Methods used by all grant types.
*/

var getAccessToken = function (bearerToken, callback) {

var tokens = config.tokens.filter(function (token) {

return token.accessToken === bearerToken
})

return callback(false, tokens[0])
}

var getClient = function (clientId, clientSecret, callback) {

var clients = config.clients.filter(function (client) {

return client.clientId === clientId && client.clientSecret === clientSecret
})

var confidentialClients = config.confidentialClients.filter(function (client) {

return client.clientId === clientId && client.clientSecret === clientSecret
})

callback(false, clients[0] || confidentialClients[0])
}

var grantTypeAllowed = function (clientId, grantType, callback) {

var clientsSource,
clients = []

if (grantType === "password") {
clientsSource = config.clients
} else if (grantType === "client_credentials") {
clientsSource = config.confidentialClients
}

if (clientsSource) {
clients = clientsSource.filter(function (client) {

return client.clientId === clientId
})
}

callback(false, clients.length)
}

var saveAccessToken = function (accessToken, clientId, expires, user, callback) {

config.tokens.push({
accessToken: accessToken,
expires: expires,
clientId: clientId,
user: user
})

callback(false)
}

/*
* Method used only by password grant type.
*/

var getUser = function (username, password, callback) {

var users = config.users.filter(function (user) {

return user.username === username && user.password === password
})

callback(false, users[0])
}

/*
* Method used only by client_credentials grant type.
*/

var getUserFromClient = function (clientId, clientSecret, callback) {

var clients = config.confidentialClients.filter(function (client) {

return client.clientId === clientId && client.clientSecret === clientSecret
})

var user

if (clients.length) {
user = {
username: clientId
}
}

callback(false, user)
}

/**
* Export model definition object.
*/

module.exports = {
getAccessToken: getAccessToken,
getClient: getClient,
grantTypeAllowed: grantTypeAllowed,
saveAccessToken: saveAccessToken,
getUser: getUser,
getUserFromClient: getUserFromClient
}

+ 36
- 0
test/e2e-cypress/helpers/oauth2-server/swagger.yaml View File

@@ -0,0 +1,36 @@
swagger: "2.0"
host: localhost:3231
paths:
/password:
get:
summary: OAuth2 Password
security:
- oauthPassword: []
responses:
200:
description: OK
schema:
type: string
/application:
get:
summary: OAuth2 Application
security:
- oauthApplication: []
responses:
200:
description: OK
schema:
type: string
securityDefinitions:
oauthPassword:
type: oauth2
flow: password
tokenUrl: /oauth/token
oauthApplication:
type: oauth2
flow: application
tokenUrl: /oauth/token
oauthImplicit:
type: oauth2
flow: implicit
authorizationUrl: /oauth/token

+ 2
- 0
test/e2e-cypress/plugins/index.js View File

@@ -1,3 +1,4 @@
const startOAuthServer = require("../helpers/oauth2-server")
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
@@ -12,6 +13,7 @@
// the project's config changing)

module.exports = (on, config) => {
startOAuthServer()
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

+ 7
- 0
test/e2e-cypress/support/index.js View File

@@ -18,3 +18,10 @@ import "./commands"

// Alternatively you can use CommonJS syntax:
// require('./commands')


// Remove fetch, so Cypress can intercept XHRs
// see https://github.com/cypress-io/cypress/issues/95
Cypress.on("window:before:load", win => {
win.fetch = null
})

test/e2e-cypress/tests/deep-linking.js → test/e2e-cypress/tests/features/deep-linking.js View File


+ 55
- 0
test/e2e-cypress/tests/features/oauth2-flows/application.js View File

@@ -0,0 +1,55 @@
describe("OAuth2 Application flow", function() {
beforeEach(() => {
cy.server()
cy.route({
url: "**/oauth/*",
method: "POST"
}).as("tokenRequest")
})

it("should make an application flow Authorization header request", () => {
cy
.visit("/?url=http://localhost:3231/swagger.yaml")
.get(".btn.authorize")
.click()

.get("div.modal-ux-content > div:nth-child(2)").within(() => {
cy.get("#client_id")
.clear()
.type("confidentialApplication")

.get("#client_secret")
.clear()
.type("topSecret")

.get("button.btn.modal-btn.auth.authorize.button")
.click()
})

cy.get("button.close-modal")
.click()

.get("#operations-default-get_application")
.click()

.get(".btn.try-out__btn")
.click()

.get(".btn.execute")
.click()

cy.get("@tokenRequest")
.its("request")
.its("body")
.should("equal", "grant_type=client_credentials")

cy.get("@tokenRequest")
.its("request")
.its("headers")
.its("authorization")
.should("equal", "Basic Y29uZmlkZW50aWFsQXBwbGljYXRpb246dG9wU2VjcmV0")

.get(".live-responses-table .response-col_status")
.contains("200")
})
})

+ 122
- 0
test/e2e-cypress/tests/features/oauth2-flows/password.js View File

@@ -0,0 +1,122 @@
describe("OAuth2 Password flow", function() {
beforeEach(() => {
cy.server()
cy.route({
url: "**/oauth/*",
method: "POST"
}).as("tokenRequest")
})

it("should make a password flow Authorization header request", () => {
cy
.visit("/?url=http://localhost:3231/swagger.yaml")
.get(".btn.authorize")
.click()

.get("#oauth_username")
.type("swagger")

.get("#oauth_password")
.type("password")

.get("#password_type")
.select("basic")

.get("#client_id")
.clear()
.type("application")

.get("#client_secret")
.clear()
.type("secret")

.get("div.modal-ux-content > div:nth-child(1) > div > div:nth-child(2) > div > div.auth-btn-wrapper > button.btn.modal-btn.auth.authorize.button")
.click()

.get("button.close-modal")
.click()

.get("#operations-default-get_password")
.click()

.get(".btn.try-out__btn")
.click()

.get(".btn.execute")
.click()

cy.get("@tokenRequest")
.its("request")
.its("body")
.should("include", "grant_type=password")
.should("include", "username=swagger")
.should("include", "password=password")
.should("not.include", "client_id")
.should("not.include", "client_secret")

cy.get("@tokenRequest")
.its("request")
.its("headers")
.its("authorization")
.should("equal", "Basic YXBwbGljYXRpb246c2VjcmV0")

.get(".live-responses-table .response-col_status")
.contains("200")
})

it("should make a Password flow request-body request", () => {
cy
.visit("/?url=http://localhost:3231/swagger.yaml")
.get(".btn.authorize")
.click()

.get("#oauth_username")
.type("swagger")

.get("#oauth_password")
.type("password")

.get("#password_type")
.select("request-body")

.get("#client_id")
.clear()
.type("application")

.get("#client_secret")
.clear()
.type("secret")

.get("div.modal-ux-content > div:nth-child(1) > div > div:nth-child(2) > div > div.auth-btn-wrapper > button.btn.modal-btn.auth.authorize.button")
.click()

.get("button.close-modal")
.click()

.get("#operations-default-get_password")
.click()

.get(".btn.try-out__btn")
.click()

.get(".btn.execute")
.click()

cy.get("@tokenRequest")
.its("request")
.its("body")
.should("include", "grant_type=password")
.should("include", "username=swagger")
.should("include", "password=password")
.should("include", "client_id=application")
.should("include", "client_secret=secret")

cy.get("@tokenRequest")
.its("request")
.its("headers")
.should("not.have.property", "authorization")

.get(".live-responses-table .response-col_status")
.contains("200")
})
})

Loading…
Cancel
Save