Skip to content

Commit

Permalink
Merge pull request #4 from pluralsight-projects/master
Browse files Browse the repository at this point in the history
grab from parent
  • Loading branch information
snychka authored May 21, 2020
2 parents 3649ac5 + 98cb6a8 commit 6b7959c
Show file tree
Hide file tree
Showing 16 changed files with 709 additions and 1 deletion.
15 changes: 15 additions & 0 deletions client.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const express = require("express")
const bodyParser = require("body-parser")
const axios = require("axios").default
const { randomString, timeout } = require("./utils")

const config = {
port: 9000,
Expand All @@ -18,6 +19,7 @@ let state = ""
const app = express()
app.set("view engine", "ejs")
app.set("views", "assets/client")
app.use(timeout)
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))

Expand All @@ -29,3 +31,16 @@ const server = app.listen(config.port, "localhost", function () {
var host = server.address().address
var port = server.address().port
})

// for testing purposes

module.exports = {
app,
server,
getState() {
return state
},
setState(s) {
state = s
},
}
121 changes: 121 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1",
"mocha": "^7.1.2",
"moxios": "^0.4.0",
"sinon": "^9.0.2",
"supertest": "^4.0.2"
}
}
2 changes: 2 additions & 0 deletions protected-resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ const server = app.listen(config.port, "localhost", function () {
var port = server.address().port
})

// for testing purposes

module.exports = {
app,
server,
Expand Down
74 changes: 74 additions & 0 deletions tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,77 @@ Note:
- The user information is declared at the beginning of the file in the `users` object. With the usernames as keys and information as an object. For example, information about the user with username `john` is present in `users["john"]`
- To get the field names from the permissions, use the `.slice()` method of strings to remove the `"permissions:"` prefix.
- Use the `res.json` method to return the object as a JSON response.

## 3. Building the Client Application

### 1. Creating the authorization route

First, we need to create the initial route that the user will hit when authorizing our application. In `client.js` create a new route that accepts `GET` requests to the `/authorize` endpoint using the `app.get()` method.

### 2. Declaring the state

We need to generate a random string and assign it to the `state` variable, in order to keep track and verify the authorization request.

Notes:

- You can use the `randomString()` function imported from `utils.js` to generate a random string.
- The random string needs to be assigned to the `state` variable, which has already been declared at the top of the file.

### 3. Redirecting the user to the authorization endpoint

Finally, we need to return a redirect response, sending the user to the `/authorize` endpoint of the authorization server (which we completed in module 2). The redirect URL needs to contain the following query parameters:

- `response_type` which is set to `"code"`
- `client_id` which is set to `config.clientId`
- `client_secret` which is set to `config.clientSecret`
- `redirect_uri` which is set to `config.redirectUri`
- `scope` which is set to `"permission:name permission:date_of_birth"`
- `state` which is set to the random string that you generated in the previous task

So if the authorization endpoint is `"http://example.com/authorize"`, the redirect URL should look something like: `"http://example.com/authorize?response_type=abc&client_id=def&client_secret=ghi&redirect_uri=lmn&scope=opq&state=rst"`

Notes:

- The `config` object is declared near the top of the file
- You can use the example under the Node.js [URLSearchParams API](https://nodejs.org/api/url.html#url_class_urlsearchparams) to see the best way to add query parameters to an existing URL string.

### 4. Creating the callback endpoint

This is the final endpoint the user is going to hit once the authorization process is complete. In `client.js` create a new route that accepts `GET` requests to the `/callback` endpoint using the `app.get()` method.

### 5. Verifying the state

The incoming request will come with a state param present in `req.query.state`. We need to verify if its value matches the random string generated and sent in the previous tasks. If the value of the state sent in the request, and the value stored in the local `state` variable declared previously don't match, send a `403` (forbidden) status code, and return.

### 6. Requesting for the access token

Once the state is verified we need to get the access token from the authorization server. After the state verification, send an HTTP `POST` request to the token endpoint URL (which can be found in the `tokenEndpoint` attribute in `config` object declared near the top of the file)

You can make use of the `axios` library to make the HTTP call. The `axios` variable is already imported at the top of the file. You can create the request by calling the `axios(requestConfig)` function. `requestConfig` here is an object containing the following parameters:

- `method` which should be set to `POST`
- `url` which should be set to `config.tokenEndpoint`
- `auth`, which is an object containing the `username` and `password` attributes, which should be set to `config.clientId` and `config.clientSecret` repectively.
- `data` which is an object that has a `data` attribute, whose value should be `req.query.code` (which is the authorization code that we get from the request)

Note:

- The `axios(requestConfig)` function returns a javascript Promise object. The response can be accessed inside the `.then(res =>{})` method of the promise. You can find more usage examples [here](https://github.com/axios/axios#example)

### 7. Requesting for user information

We can now use the access token to request for the users scoped information. The response from the previous task will contain an access token in the `response.data.access_token` attribute.

Create an HTTP `GET` request to the user info endpoint by using the `axios(requestConfig)` function. `requestConfig` here is an object containing the following parameters:

- `method` which should be set to `GET`
- `url` which should be set to `config.userInfoEndpoint`
- `headers`, which is an object with the `authorization` attribute, who's value should be `"bearer <access_token>"` where `<access_token>` should be replaced with the access token from the response data.

Similar to the last step, the response to this request will be present in the `then()` method of the promise object.

### 8. Rendering the welcome page

The response to the user-info request should contain the relevant personal information of the user. We can now render the welcome page.

You can render the welcome page using the `res.render(page, params)`. The `page` argument can be set to `"welcome"` which will render the `assets/client/welcome.ejs` template file. The `params` argument is needed to supply parameters to the template file. In this case `params` needs to be an object with a `user` attribute, whose value would be the data present in the user-info response, which can be accessed in the `response.data` attribute.
69 changes: 69 additions & 0 deletions test/module1/15-verify-jwt-issue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const assert = require("assert")
const sinon = require("sinon")
const request = require("supertest")
const fs = require("fs")
const jwt = require("jsonwebtoken")

const { app, authorizationCodes } = require("../../authorization-server")
const { deleteAllKeys } = require("../../utils")

const publicKey = fs.readFileSync("assets/public_key.pem")

after(() => {
sinon.restore()
})

it("/token should issue the appropriate JWT if authorization succeeds @authorization-server-verify-jwt-issue", () => {
deleteAllKeys(authorizationCodes)
sinon.replace(
jwt,
"sign",
sinon.fake.returns(
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6ImpvaG4iLCJzY29wZSI6InBlcm1pc3Npb246bmFtZSIsImlhdCI6MTU4OTczMTA4MCwiZXhwIjoxOTg5NzMxMzgwLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwMDEifQ.TSWuYj9fOT0d753Q6BisA4bYMoJA-_R9oL3OHTy6PjucidfjGuwpXjvD6Bg-kAYIrJf7Z-b7agv65fB0-Xi2sR48HaVYjmva5qI9lbnSyMDyyynaIiUQ2u4mCfZ4NF0Xo06v8QkVzyKZlS4SFOnYZwVAcywSxa4KDI9AkrW-7G8Vo1RNYp8ztsl5bCy8etk8I10S1-ikb0vu4RUSIa3ge2TS5ZsDXzabn3Yb8U5RygE7C_Qxj_xQN-Jff0fKrCl02NNm0v88jTNmQihPiZid7wVi0CTpwlj-CspQPm-flypcWJIHOJOn7yzdScNwAZzqhJtwlZFuDuGvTgg-B5FZbQ"
)
)
const code = "somerandomcode"
authorizationCodes[code] = {
userName: "john",
clientReq: { scope: "permission:name" },
}
return request(app)
.post("/token")
.set("authorization", "Basic dGVzdC1jbGllbnQ6VGVzdFNlY3JldA==")
.send({ code })
.then((res) => {
assert.equal(
jwt.sign.callCount,
1,
"/token should call the `jwt.sign` method to issue the JWT"
)
const { args } = jwt.sign.getCall(0)
const payload = args[0]
const jwtConfig = args[2]
assert.equal(
payload !== null && typeof payload === "object",
true,
"JWT payload should be an object"
)
assert.equal(
payload.userName,
"john",
"/token should set the correct userName on the JWT payload"
)
assert.equal(
payload.scope,
"permission:name",
"/token should se the correct scope on the JWT payload"
)
assert.equal(
jwtConfig !== null && typeof jwtConfig === "object",
true,
"JWT config should be an object"
)
assert.equal(
jwtConfig.algorithm,
"RS256",
'JWT algorithm should be set to "RS256"'
)
})
})
File renamed without changes.
20 changes: 20 additions & 0 deletions test/module3/01-create-authorization-route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const assert = require("assert")
const request = require("supertest")

const { app, server } = require("../../client")

it("serves an empty authorization route @client-create-authorization-route", () => {
return request(app)
.get("/authorize")
.then((res) => {
assert.notEqual(
res.status,
404,
"The `/authorize` route doesn't exist"
)
})
})

afterEach(() => {
server.close()
})
Loading

0 comments on commit 6b7959c

Please sign in to comment.