- Creating the authorization route - In
authorization-server.js
create a new empty server route that acceptsGET
requests to the/authorize
endpoint using theapp.get
method. The empty route should return a200
status by default, using theres.end()
method. - Verifying the client ID - Next, get the
client_id
param from thereq.query
object and verify if the client ID exists by checking theclients
object in the same file. If the client ID does not exist, respond with a401
status code using theres.status
method. If the client ID exists, return a200
status. - Validating the scopes requested - After the client ID is validated, we need to ensure that the requested scopes are a subset of the allowed scopes for the client. For example, the client with ID
"my-client"
is allowed to request for the"permission:name"
, and the"permission:date_of_birth"
scopes. The requested scopes are available in thereq.query.scopes
parameter as a space separated string. We can split this string into it's individual scopes by using thereq.query.scope.split(" ")
method. You can use thecontainsAll(arg1, arg2)
function imported from theutils.js
file to check if the arrayarg1
contains all the elements of the arrayarg2
. If requested scopes are not a subset of the allowed scopes, return a401
status code. - Storing the request - Now that we have verified all the client credentials and scope, we need to create a request ID and temporarily store the request object to use in the next section. You can see an empty
requests
object already declared in the file. We need to create a random string as the request ID, which will be a key stored in therequests
object, with the value being thereq.query
object. To help you generate a random string, you can use therandomString()
function in theutils.js
file. - Rendering the login page - We now need to render the login page for the user to login to our system. You can render the login page using the
res.render(page, params)
. Thepage
argument can be set to"login"
which will render theassets/authorization-server/login.ejs
template file. Theparams
argument is needed to supply parameters to the template file. In this caseparams
needs to be an object with three keys:"client"
, whose value will be the client ID;"scope"
, whose value would be set to the value ofreq.query.scope
;"requestId"
whose value would be set to the value of the random request ID string generated in the previous task. - Creating the approve route - Once the user goes to the login page rendered in the last step, they will be shown a login form which will send a request to the approval endpoint. Create a new server route that accepts
POST
requests to the/approve
endpoint using theapp.post()
method. - Verifying the username and password - The
POST
request contains theuserName
,password
andrequestID
keys in thereq.body
object. We need to check if theuserName
andpassword
match. The usernames and passwords are listed in theusers
variable near the beginning of the file, with the usernames as keys and the passwords as values (for examplejohn
is a username withappleseed
as their password). If the usernames and passwords don't match, return a401
status. - Checking if the request exists - We now have to check if the request with the given
requestId
(that was provided inreq.body
) actually exists. We can do this by checking if a key corresponding torequestId
exists in therequests
object (usingrequests[requestId]
). If it doesn't exist, return a401
status. If it does exist, assign it to a local variable (for use in the next task) and delete it from therequests
object. - Storing the request and userName - Next we need to retain the client request we obtained from the previous task (the value stored in
requests[requestId]
), and theuserName
of the logged in user in theauthorizationCodes
local variable.authorizationCodes
is an empty object declared near the start of the file. Generate a random string (with therandomString()
function) and set it as a new key inauthorizationCodes
, with the value being an object with two attributes:"clientReq"
, whose value is the client request object, and"userName"
, whose value is theuserName
of the logged in user. This random string will be the "code" which we will use in the next task. - Redirecting the user - The client request object will contain a
redirectUri
, and astate
attribute. We need to send a redirect response to this redirect URI using theres.redirect()
method of the express.js response object. We also need to add the code generated in the last step and the state as query params to the redirect URI. For example, if theredirectUri
value ishttp://www.example.com/go-here
, our code is"rof5ijf"
, and the state is"pc03ns9S"
, we need to redirect tohttp://www.example.com/go-here?code=rof5ijf&state=pc03ns9S
- Creating the token route - Now that we can issue an authorization code to the client redirect URI, we need to create a
/token
route to issue the authorization token using the authorization code. Create a new server route that acceptsPOST
requests to the/token
endpoint using theapp.post()
method. - Checking if authorization credentials exist - We expect this endpoint to receive an authorization token in the
req.headers.authorization
attribute. Check if theauthorization
header exists. If not, return a401
status. - Verifying the authorization header - Each authorization header is encoded with the standard basic auth algorithm. A helper function
decodeAuthCredentials
is present in theutils.js
file, which accepts the auth token and returns and object containingclientId
andclientSecret
keys. We need to check if these match the client IDs and secrets stored with us. This can be found in theclients
object at the start of the file. For example,my-client
is a client ID whose client secret iszETqHgl0d7ThysUqPnaFuLOmG1E=
. If the client ID and secret don't match, return a401
status. - Verifying the authorization code - The
req.body
object contains thecode
key attribute, whose value corresponds to the code we issued in the last route. We need to check if an object that matches this code exists in theauthorizationCodes
object. For example, if thereq.body.code
is"abc"
, we need to check ifobj = authorizationCodes["abc"]
exists. If it doesn't, return a401
status. If it exists, save the value in a local variable (for later use) and delete the key fromauthorizationCodes
. - Issuing the access token - Once all the above info has been validated, we need to create a signed token, and return it as the response. Create a new JWT using the
jwt.sign
method from the"jsonwebtoken"
library (you can use other libraries if you prefer as well). The object we need to encode needs to contain the"userName"
and"scope"
keys, which can be obtained from the object we extracted fromauthorizationCodes
in the last step (asobj.userName
andobj.clientReq.scope
respectively). This needs to be encoded using the"RS256"
algorithm. The public and private keys for this can be found in theassets
folder aspublic_key.pem
andprivate_key.pem
. Once we create the JWT string, respond with a200
status and a JSON body with the following parameters:"access_token"
, whose value is the JWT string;"token_type"
, whose value is"Bearer"
.
The protected resource needs to contain a route that gives out information about to the user to authorized clients. In protected-resource.js
create a new server route that accepts GET
requests to the /user-info
endpoint using the app.get
method.
The identifying information about the user whose information is being requested will be contained in the authorization header present as req.headers.authorization
attribute. We need to verify that this header exists, and return a 401
status incase it doesn't.
In the previous module, we issued a JWT with an encoded object containing the userName
and scope
keys. We need to extract these keys from the token that comes along with the request. The authorization token is a string with the value authToken = "bearer <your_token_payload>"
. You can extract the token payload from the string using the authToken.slice()
method. Once you've extracted the token payload, you can use the jwt.verify
function from the jsonwebtoken
library to decode and return the encoded object containing the required keys. If the token verification fails, return a 401
status.
Notes:
- The
jwt.verify
function takes three arguments: first, the token payload string. Second, the public key (which is present inconfig.publicKey
which has been declared at the beginning of the file). Lastly, the options object, where you will have to set the"algorithms"
key to["RS256"]
.
In the previous task, we acquired the username and the scope. We now need to return the relevant information about the user as a JSON response, based on the scope. For example, if the userName
is "john"
, and the scope
is "permission:name permission:date_of_birth"
, our response should be {"name": "John Appleseed", "date_of_birth":"12th September 1998"}
.
Note:
- Use the
.split()
method of strings to get the scopes as an array from the space separated scopes string. - 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 usernamejohn
is present inusers["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.
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.
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 fromutils.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.
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 toconfig.clientId
client_secret
which is set toconfig.clientSecret
redirect_uri
which is set toconfig.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 to see the best way to add query parameters to an existing URL string.
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.
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.
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 toPOST
url
which should be set toconfig.tokenEndpoint
auth
, which is an object containing theusername
andpassword
attributes, which should be set toconfig.clientId
andconfig.clientSecret
repectively.data
which is an object that has adata
attribute, whose value should bereq.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
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 toGET
url
which should be set toconfig.userInfoEndpoint
headers
, which is an object with theauthorization
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.
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.