In OAuth, the client is identified to the authorization server by a client identifier that is, generally speaking, unique to the software application functioning as the OAuth client. This client ID is passed in the front end to the authorization endpoint during the authorization request stage of interactive OAuth flows, such as the authorization code grant type that we implemented in chapters 3 through 5. From this client ID, the authorization server can make decisions about which redirect URIs to allow, which scopes to allow, and what information to display to the end user. The client ID is also presented at the token endpoint, and when combined with a client secret the client ID can authenticate the client throughout the authorization delegation process of OAuth.
This client identifier is particularly distinct from any identifiers or accounts that may be held by the resource owner. This distinction is important in OAuth because, you’ll recall, OAuth doesn’t encourage impersonation of the resource owner. In fact, the entire OAuth protocol is all about acknowledging that a piece of software is acting on behalf of the resource owner. But how does the client get that identifier, and how does the server know how to associate metadata such as a valid set of redirect URIs or scopes with that identifier?
In all of our exercises so far, the client ID has been statically configured between the authorization server and the client; that is to say, there was an out-of-band -agreement—specifically, the text of this book—that determined ahead of time what the client ID and its associated secret would be. The server determined the client ID, which was then copied by hand into the client software. A major downside to this approach is that every instance of every piece of client software for a given API will need to be tied to every instance of an authorization server that protects that API. This is a reasonable expectation when the client and authorization server have a well-established and relatively unchanging relationship, such as when the authorization server is set up to protect a single, proprietary API. For example, back in our cloud-printing example, the user was given the option to import their photos from a specific, well-known photo-storage service. The client was specifically written to talk to this particular service. In this fairly common setup, a limited number of clients will speak to that API and static registration works well enough.
But what if a client is written to access an API that’s available across many different servers? What if our printing service could talk to any photo-storage service that spoke a standardized photo-storage API? These photo-storage services will likely have separate authorization servers, and such a client will need a client identifier with each one it talks to. We could try to say that we’ll reuse the same client ID regardless of the authorization server, but which authorization server gets to pick the ID? After all, not every authorization server will use the same pattern of choosing IDs, and we want to make sure that the chosen ID doesn’t conflict with another piece of software on any authorization server. And what happens when there’s a new client that needs to be introduced to the ecosystem? Whatever client ID it was assigned would need to be communicated to all authorization servers along with the associated metadata.
Or what if there were many instances of a piece of client software, and each instance needed to talk to the same authorization server? As we saw in chapter 6, this is the case with native applications, and each client instance will need a client identifier to talk to the authorization server. We could once again say that we’ll use the same identifier with each instance, and that can work in some cases. But what should we do about a client secret? We already know from chapter 7 that we can’t copy the same secret everywhere, since it wouldn’t be much of a secret anymore.[1] We could approach this by leaving the secret out all together and having our client be a public client, which is accepted and codified in the OAuth standard. However, public clients are open to all kinds of attacks including authorization code and token theft as well as impersonation of a genuine client by malicious software. Sometimes, this is an acceptable trade-off, but many times it isn’t and we’d like each instance to have a separate client secret.
Google famously got around the OAuth 1.0 requirement of every client needing a client secret by declaring that all native applications using Google’s OAuth 1.0 server would use the client ID “anonymous” with the client secret “anonymous.” This completely breaks the security model’s assumptions. Furthermore, Google added an extension parameter to replace the now-missing client ID, which further broke the protocol.
In either of these cases, manual registration doesn’t scale. To put the problem in perspective, consider this extreme but real example: email. Would it be reasonable for a developer to register each copy of an email client with each potential email service provider before shipping the software? After all, every single domain and host on the internet could have its own separate mail server, not to mention intranet mail services. It’s clear that this is not at all reasonable, but this is the assumption made with manual registration in OAuth. What if there were another way? Can we introduce clients to authorization servers without manual intervention?
The OAuth Dynamic Client Registration protocol[2] provides a way for clients to introduce themselves to authorization servers, including all kinds of information about the client. The authorization server can then provision a unique client ID to the client software that the client can use for all subsequent OAuth transactions, and if appropriate associate a client secret with that ID as well. This protocol can be used by the client software itself, or it could be used as part of a build and deployment system that acts on behalf of the client developer (figure 12.1).
RFC 7591 https://tools.ietf.org/html/rfc7591
The core dynamic client registration protocol is a simple HTTP request to the authorization server’s client registration endpoint and its corresponding response. This endpoint listens for HTTP POST requests with a JSON body containing a client’s proposed metadata information. This call can be optionally protected by an OAuth token, but our example shows an open registration with no authorization.
POST /register HTTP/1.1 Host: localhost:9001 Content-Type: application/json Accept: application/json { "client_name": "OAuth Client", "redirect_uris": ["http://localhost:9000/callback"], "client_uri": "http://localhost:9000/", "grant_types": ["authorization_code"], "scope": "foo bar baz" }
This metadata can include display names, redirect URIs, scopes, and many other aspects of the client’s functionality. (The full official list is included in section 12.3.1, if you’d like to read ahead.) However, the requested metadata can never include a client ID or client secret. Instead, these values are always under the control of the authorization server in order to prevent impersonation of or conflict with other clients IDs, or selection of weak client secrets. The authorization server can perform some basic consistency checks against the presented data, for example, making sure that the requested grant_types and response_types can be served together, or that the scopes requested are valid for a dynamically registered client. As is generally the case in OAuth, the authorization server gets to make the decisions about what is valid and the client, being a simpler piece of software, obeys what the authorization server dictates.
Upon a successful registration request, the authorization server generates a new client ID and, generally, a client secret. These are sent back to the client along with a copy of the metadata associated with the client. Any values that the client sends in the request are suggested input to the authorization server, but the authorization server has the final say over which values are associated with the client’s registration and is free to override or reject any inputs as it sees fit. The resulting registration is sent back to the client as a JSON object.
HTTP/1.1 201 Created Content-Type: application/json { "client_id": "1234-wejeg-0392", "client_secret": "6trfvbnklp0987trew2345tgvcxcvbjkiou87y6t5r", "client_id_issued_at": 2893256800, "client_secret_expires_at": 0, "token_endpoint_auth_method": "client_secret_basic", "client_name": "OAuth Client", "redirect_uris": ["http://localhost:9000/callback"], "client_uri": "http://localhost:9000/", "grant_types": ["authorization_code"], "response_types": ["code"], "scope": "foo bar baz" }
In this example, the authorization server has assigned a client ID of 1234-wejeg-0392 and a client secret of 6trfvbnklp0987trew2345tgvcxcvbjkiou87y6t5r to this client. The client can now store these values and use them for all subsequent communications with the authorization server. Additionally, the authorization server has added a few things to the client’s registration record. First, the token_endpoint_auth_method value indicates that the client should use HTTP Basic authentication when talking to the token endpoint. Next, the server has filled in the missing response_types value to correspond to the grant_types value from the client’s request. Finally, the server has indicated to the client when the client ID was generated and that the client secret won’t expire.
There are several compelling reasons for using dynamic registration with OAuth. The original OAuth use cases revolved around single-location APIs, such as those from companies providing web services. These APIs require specialized clients to talk to them, and those clients will need to talk to only a single API provider. In these cases, it doesn’t seem unreasonable to expect client developers to put in the effort to register their client with the API, because there’s only one provider.
But you’ve already seen two major exceptions to this pattern wherein these assumptions don’t hold. What if there’s more than one provider of a given API, or new instances of that same API can be stood up at will? For example, OpenID Connect provides a standardized identity API, and the System for Cross-domain Identity Management (SCIM) protocol provides a standardized provisioning API. Both of these are protected by OAuth, and both can be stood up by different providers. Although a piece of client software could talk to these standard APIs no matter what domains they were running on, we know that managing client IDs in this space is unfeasible. Simply put, writing a new client or deploying a new server for this protocol ecosystem would be a logistical nightmare.
Even if we have a single authorization server to deal with, what about multiple instances of a given client? This is particularly pernicious with native clients on mobile platforms, because every copy of the client software would have the same client ID and client secret. With dynamic registration, each instance of a client can register itself with the authorization server. Each instance then gets its own client ID and, importantly, its own client secret that it can use to protect its user.
Remember that we said that the kind of interactions email clients have with servers is a driving use case for dynamic registration. Today, OAuth can be used to access Internet Message Access Protocol (IMAP) email services using a Simple Authentication and Security Layer–Generic Security Service Application Program Interface (SASL-GSSAPI) extension. Without dynamic registration, every single mail client would have to preregister with every single possible email provider that allowed OAuth access. This registration would need to be completed by the developer before the software ships, because the end user won’t be able to modify and configure the email client once it’s installed. The possible combinations are staggering for both authorization servers that need to know about every mail client and for mail clients that need to know about every server. Better, instead, to use dynamic client registration, in which each instance of a mail client can register itself with each instance of an authorization server that it needs to talk to.
It may seem intimidating to allow dynamic registration on an authorization server. After all, do you want any piece of software to waltz up and start asking for tokens? The truth is, oftentimes you want to do exactly that. Interoperability is by its very nature indistinguishable from an unsolicited request.
Importantly, a client being registered at the authorization server doesn’t entitle that client to access any resources protected by that authorization server. Instead, a resource owner still needs to delegate some form of access to the client itself. This key fact differentiates OAuth from other security protocols wherein the registration event carries with it authority to access resources and therefore needs to be protected by a strict onboarding process.
For clients that have been vetted by administrators of the authorization server, and have been statically registered by such trusted authorities, the authorization server might want to skip prompting the resource owner for their consent. By placing certain trusted clients on a whitelist, the authorization server can smooth the user experience for these clients. The OAuth protocol works exactly the same as before: the resource owner is redirected to the authorization endpoint, where they authenticate, and the authorization server reads the access request in the front channel. But instead of prompting the user for their decision about a trusted client, the authorization server can have policy decide that a client is already authorized, and return the result of the authorization request immediately.
At the other end of the spectrum, an authorization server can decide that it never wants to let clients with particular attributes register or request authorization. This can be a set of redirect URIs that are known to house malicious software, or display names that are known to be intentionally confusing to end users, or other types of detectable malice. By placing these attribute values on a blacklist, the authorization server can prevent clients from ever using them.
Everything else can go on a graylist, whereby resource owners make the final authorization decisions. Dynamically registered clients that don’t fall under the blacklist and have not yet been whitelisted should automatically fall on the graylist. These clients can be more limited than statically registered clients, such as not having the ability to ask for certain scopes or use certain grant types, but otherwise they function as normal OAuth clients. This will allow greater scalability and flexibility for an authorization server without compromising its security posture. A dynamically registered client used successfully by many users over a sufficiently long period of time could eventually become whitelisted, and malicious clients could find their registrations revoked and their key attributes blacklisted.
Now that you know how the protocol works, we’re going to implement it. We’ll start on the server side with the registration endpoint. Open up ch-12-ex-1 and edit authorizationServer.js for this part of the exercise. Our authorization server will use the same in-memory array for client functionality that it did in chapter 5, meaning this storage will reset whenever the server is restarted. In contrast, a production environment is likely to want to use a database or some other, more robust storage mechanism.
First, we’ll need to create the registration endpoint. On our server, this listens for HTTP POST requests on the /register URL, so we’ll set up a handler for that. In our server, we’re only going to be implementing public registration, which means we’re not going to be requiring the optional OAuth access token at our registration endpoint. We’re also going to set up a variable to collect the incoming client metadata requests as we process them.
app.post('/register', function (req, res){ var reg = {}; });
The Express.js code framework in our application is set up to automatically parse the incoming message as a JSON object, which is made available to the code in the req.body variable. We’re going to do a handful of basic consistency checks on the incoming data. First, we’ll see what the client has asked for as an authentication method. If it hasn’t specified one, we’re going to default to using a client secret over HTTP Basic. Otherwise, we’ll take the input value specified by the client. We’ll then check to make sure that value is valid, and return an invalid_client_metadata error if it is not. Note that the values for this field, like secret_basic, are defined by the specification and can be extended with new definitions.
if (!req.body.token_endpoint_auth_method) { reg.token_endpoint_auth_method = 'secret_basic'; } else { reg.token_endpoint_auth_method = req.body.token_endpoint_auth_method; } if (!__.contains(['secret_basic', 'secret_post', 'none'], reg.token_endpoint_auth_method)) { res.status(400).json({error: 'invalid_client_metadata'}); return; }
Next, we’ll read in the grant_type and response_type values and ensure that they are consistent. If the client doesn’t specify either, we’ll default them to an authorization code grant. If they request a grant_type but not its corresponding response_type, or vice versa, we’ll fill in the missing value for them. The specification defines not only the appropriate values but also the relationship between these two values. Our simple server supports only the authorization code and refresh token grants, so we’re going to send back an invalid_client_metadata error if they request anything else.
if (!req.body.grant_types) { if (!req.body.response_types) { reg.grant_types = ['authorization_code']; reg.response_types = ['code']; } else { reg.response_types = req.body.response_types; if (__.contains(req.body.response_types, 'code')) { reg.grant_types = ['authorization_code']; } else { reg.grant_types = []; } } } else { if (!req.body.response_types) { reg.grant_types = req.body.grant_types; if (__.contains(req.body.grant_types, 'authorization_code')) { reg.response_types =['code']; } else { reg.response_types = []; } } else { reg.grant_types = req.body.grant_types; reg.reponse_types = req.body.response_types; if (__.contains(req.body.grant_types, 'authorization_code') && !__.contains(req.body.response_types, 'code')) { reg.response_types.push('code'); } if (!__.contains(req.body.grant_types, 'authorization_code') && __.contains(req.body.response_types, 'code')) { reg.grant_types.push('authorization_code'); } } } if (!__.isEmpty(__.without(reg.grant_types, 'authorization_code', refresh_token')) || !__.isEmpty(__.without(reg.response_types, 'code'))) { res.status(400).json({error: 'invalid_client_metadata'}); return; }
Next, we’ll make sure that the client has registered at least one redirect URI. We enforce this on all clients because this version of the server supports only the authorization code grant type, which is based on a redirect. If you were supporting other grant types that don’t use redirection, you’d want to make this check conditional on the grant type. If you were checking redirect URIs against a blacklist, this would be a good place to implement that functionality as well, but implementing that type of filtering is left as an exercise to the reader.
if (!req.body.redirect_uris || !__.isArray(req.body.redirect_uris) || __.isEmpty(req.body.redirect_uris)) { res.status(400).json({error: 'invalid_redirect_uri'}); return; } else { reg.redirect_uris = req.body.redirect_uris; }
Next, we’ll copy over the other fields that we care about, checking their data types on the way. Our implementation will ignore any additional fields passed in that it doesn’t understand, though a production quality implementation might want to hang on to extra fields in case additional functionality is added to the server at a later time.
if (typeof(req.body.client_name) == 'string') { reg.client_name = req.body.client_name; } if (typeof(req.body.client_uri) == 'string') { reg.client_uri = req.body.client_uri; } if (typeof(req.body.logo_uri) == 'string') { reg.logo_uri = req.body.logo_uri; } if (typeof(req.body.scope) == 'string') { reg.scope = req.body.scope; }
Finally, we’ll generate a client ID and, if the client is using an appropriate token endpoint authentication method, a client secret. We’ll also note the registration timestamp and mark that the secret doesn’t expire. We’ll attach these directly to our client registration object that we’ve been building up.
reg.client_id = randomstring.generate(); if (__.contains(['client_secret_basic', 'client_secret_post']), reg.token_endpoint_auth_method) { reg.client_secret = randomstring.generate(); } reg.client_id_created_at = Math.floor(Date.now() / 1000); reg.client_secret_expires_at = 0;
Now we can store that client object in our client storage. As a reminder, we’re using a simple in-memory array, but a production system would probably be using a database for this part. After we’ve stored it, we return the JSON object to the client.
clients.push(reg); res.status(201).json(reg); return;
Once it’s all put together, our registration endpoint looks like listing 13 in appendix B.
Our authorization server’s registration system is simple, but it can be augmented to do other checks on the client such as checking all URLs against a blacklist, limiting the scopes available to dynamically registered clients, ensuring the client provides a contact address, or any number of other checks. The registration endpoint can also be protected by an OAuth token, thereby associating the registration with the resource owner who authorized that token. These enhancements are left as an exercise to the reader.
Now we’re going to set up our client to register itself as needed. Using the previous exercise, edit client.js. Near the top of the file, note that we’ve set aside an empty object for storing client information:
var client = {};
Instead of filling it in by hand as we did in chapter 3, we’re going to use the dynamic registration protocol. Once again, this is an in-memory storage solution that gets reset every time the client software is restarted, and a production system will likely use a database or other storage mechanism for this role.
First, we have to decide whether we need to register, because we don’t want to register a new client every time we need to talk to the authorization server. When the client is about to send the initial authorization request, it first checks to see whether it has a client ID for the authorization server. If it doesn’t have one, it calls a utility function to handle client registration. If the registration was successful, the client continues on. If it wasn’t, the client renders an error and gives up. This code is already included in the client.
if (!client.client_id) { registerClient(); if (!client.client_id) { res.render('error', {error: 'Unable to register client.'}); return; } }
Now we’ll be defining that registerClient utility function. This is a simple function that will POST a registration request to the authorization server and store the response in the client object.
var registerClient = function() { };
First, we need to define the metadata values that we’re sending to the authorization server. These act as a kind of template for our client’s configuration, and the authorization server will fill in the other required fields such as the client ID and client secret for us.
var template = { client_name: 'OAuth in Action Dynamic Test Client', client_uri: 'http://localhost:9000/', redirect_uris: ['http://localhost:9000/callback'], grant_types: ['authorization_code'], response_types: ['code'], token_endpoint_auth_method: 'secret_basic' };
We send this template object over to the server in an HTTP POST request.
var headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' }; var regRes = request('POST', authServer.registrationEndpoint, { body: JSON.stringify(template), headers: headers } );
Now we’ll check the result object. If we get back an HTTP code of 201 Created, we save the returned object to our client object. If we get back an error of any kind, we don’t save the client object, and we allow whatever function called us to pick up on the error state of the client being unregistered and handle it appropriately.
if (regRes.statusCode == 201) { var body = JSON.parse(regRes.getBody()); console.log("Got registered client", body); if (body.client_id) { client = body; } }
From here, the rest of the application takes over as normal. No further changes are needed to the calls to the authorization server, the processing of tokens, or access of the protected resource (figure 12.2). The client name that you registered now shows up on the authorization screen, as does the dynamically generated client ID. To test this, edit the client’s template object, restart the client, and run the test again. Note that you don’t need to restart the authorization server for the registration to succeed. Since the authorization server can’t identify the client software making the request, it will happily accept a new registration request from the same client software many times, issuing a new client ID and secret each time.
Some clients need to be able to get tokens from more than one authorization server. For an additional exercise, refactor the client’s storage of its registration information such that it’s dependent on the authorization server that the client is talking to. For an additional challenge, implement this using a persistent database instead of an in-memory storage mechanism.
The attributes associated with a registered client are collectively known as its client metadata. These attributes include those that affect the functionality of the underlying protocol, such as redirect_uris and token_endpoint_auth_method, as well as those that affect the user experience, such as client_name and logo_uri. As you’ve seen in the previous examples, these attributes are used in two different ways in the dynamic registration protocol:
As in most places in OAuth, the client is subservient to the authorization server. The client can request, but the authorization server dictates the final reality.
The core dynamic client registration protocol defines a set of common client metadata names, and this set can be extended. For example, the OpenID Connect Dynamic Client Registration specification, which is based on and compatible with OAuth Dynamic Client Registration, extends this list with a few more of its own, specific to the OpenID Connect protocol that we’ll cover chapter 13. We’ve included a few OpenID Connect specific extensions in table 12.1 that have general applicability to OAuth clients.
Among the various possible pieces of client information that are sent in a registration request and response, several are intended to be presented to the resource owner on the authorization page or other user-facing screens on the authorization server. These consist of either strings that are displayed directly for the user (such as client_name, the display name of the client software) or URLs for the user to click on (such as client_uri, the client’s homepage). But if a client can be used in more than one language or locale, it could have a version of these human-readable values for each language that it supports. Would such a client need to register separately for each language family?
Thankfully not, as the dynamic client registration protocol has a system (borrowed from OpenID Connect) for representing values in multiple languages simultaneously. In a plain claim, such as client_name, the field and value will be stored as a normal JSON object member:
"client_name": "My Client"
In order to represent a different language or script, the client also sends a version of the field with the language tag appended to the field name with the # (pound or hash) character. For example, let’s say that this client is known as “Mon Client” in French. The language code for French is fr, and so the field would be represented as client_name#fr in the JSON object. These two fields would be sent together.
"client_name": "My Client", "client_name#fr": "Mon Client"
The authorization server should use the most specific entry possible in interacting with users. For instance, if a user on the authorization server has registered their preferred language as French, the authorization server would display the French version of the name in lieu of the generic version. The client should always provide the generic version of a field name, because if nothing more specific is available, or if international locales aren’t supported, the authorization server will display the generic text with no locale qualifier.
Implementation and use of this feature are left as an exercise to the reader, since it requires a bit of fiddling with the client’s data model and the web server’s locale settings to be useful. Although some programming languages are able to automatically parse JSON objects into native objects in the language platform, and thereby offer native object-member access to the values, the # character used in this internationalization method is often not a valid character for object method names. Therefore, alternative access methods need to be used. For example, in JavaScript, the first value in the previous object could be accessed as client.client_name, but the second value would need to be accessed as client[“client_name#fr”] instead.
Every metadata value that a client sends in a dynamic registration request needs to be considered completely self-asserted. In such circumstances, there is nothing preventing a client from claiming a misleading client name or a redirect URI on someone else’s domain. As you saw in chapters 7 and 9, this can lead to a whole range of vulnerabilities if the authorization server isn’t careful.
But what if we had a way to present client metadata to the authorization server in a way that the authorization server could verify that it’s coming from a trusted party? With such a mechanism, the authorization server would be able to lock down certain metadata attributes in clients and have a higher assurance that the metadata is valid. The OAuth dynamic registration protocol provides such a mechanism in the software statement.
Simply put, a software statement is a signed JWT that contains as its payload client metadata as it would be found inside a request to the registration endpoint, as we saw in section 12.2. Instead of manually registering each instance of a piece of client software with all authorization servers, the client developer can instead preregister some subset of their client’s metadata, particularly the subset not likely to change over time, at a trusted third party and be issued a software statement signed by the trusted party. The client software can then present this software statement along with any additional metadata required for registration to the authorization servers that it registers at.
Let’s take a look at a concrete example. Suppose that a developer wants to preregister a client such that the client name, client homepage, logo, and terms of service are constant across all instances of the client and across all authorization servers. The developer registers these fields with a trusted authority and is issued a software statement in the form of a signed JWT.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb2Z0d2FyZV9pZCI6Ijg0MDEyLTM5MTM0LTM5 MTIiLCJzb2Z0d2FyZV92ZXJzaW9uIjoiMS4yLjUtZG9scGhpbiIsImNsaWVudF9uYW1lIjoiU3BlY 2lhbCBPQXV0aCBDbGllbnQiLCJjbGllbnRfdXJpIjoiaHR0cHM6Ly9leGFtcGxlLm9yZy8iLCJsb2 dvX3VyaSI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvbG9nby5wbmciLCJ0b3NfdXJpIjoiaHR0cHM6Ly9 leGFtcGxlLm9yZy90ZXJtcy1vZi1zZXJ2aWNlLyJ9.X4k7X-JLnOM9rZdVugYgHJBBnq3s9RsugxZ QHMfrjCo
The payload of this JWT decodes into a JSON object much like one that would be sent in a registration request.
{ "software_id": "84012-39134-3912", "software_version": "1.2.5-dolphin", "client_name": "Special OAuth Client", "client_uri": "https://example.org/", "logo_uri": "https://example.org/logo.png", "tos_uri": "https://example.org/terms-of-service/" }
The registration request sent by the client can contain additional fields not found in the software statement. In this example, client software can be installed on different hosts, necessitating different redirect URIs, and be configured to access different scopes. A registration request for this client would include its software statement as an additional parameter.
POST /register HTTP/1.1 Host: localhost:9001 Content-Type: application/json Accept: application/json { "redirect_uris": ["http://localhost:9000/callback"], "scope": "foo bar baz", "software_statement": " eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb2Z0d2FyZV 9pZCI6Ijg0MDEyLTM5MTM0LTM5MTIiLCJzb2Z0d2FyZV92ZXJzaW9uIjoiMS4yLjUtZG9scGhp biIsImNsaWVudF9uYW1lIjoiU3BlY2lhbCBPQXV0aCBDbGllbnQiLCJjbGllbnRfdXJpIjoiaH R0cHM6Ly9leGFtcGxlLm9yZy8iLCJsb2dvX3VyaSI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvbG9n by5wbmciLCJ0b3NfdXJpIjoiaHR0cHM6Ly9leGFtcGxlLm9yZy90ZXJtcy1vZi1zZXJ2aWNlLy J9.X4k7X-JLnOM9rZdVugYgHJBBnq3s9RsugxZQHMfrjCo" }
The authorization server will parse the software statement, validate its signature, and determine that it has been issued by an authority it trusts. If so, the claims inside the software statement will supersede those presented in the unsigned JSON object.
Software statements allow for a level of trust beyond the self-asserted values usually found in OAuth. They also allow a network of authorization servers to trust a central authority (or several) to issue software statements for different clients. Furthermore, multiple instances of a client can be logically grouped together at the authorization server by the information in the software statement that they all present. Although each instance would still get its own client ID and client secret, a server administrator could have an option to disable or revoke all copies of a given piece of software at once in the case of malicious behavior on the part of any instance.
Implementation of software statements is left as an exercise to the reader.
A client’s metadata doesn’t always remain static over time. Clients could change their display names, add or remove redirect URIs, require a new scope for new functionality, or make any number of other changes over the lifetime of the client. The client might want to read its own configuration, too. If the authorization server rotates a client’s secret after some amount of time or a triggering event, the client will need to know the new secret. Finally, if a client knows that it’s not going to be used again, such as when it’s getting uninstalled by the user, it can tell the authorization server to get rid of its client ID and associated data.
For all of these use cases, the OAuth Dynamic Client Registration Management protocol[3] defines a RESTful protocol extension to OAuth Dynamic Client Registration. The management protocol extends the core registration protocol’s “create” method with associated “read,” “update,” and “delete” methods, allowing for full lifecycle management of dynamically registered clients.
RFC 7592 https://tools.ietf.org/html/rfc7592
To accomplish this, the management protocol extends the response from the registration endpoint with two additional fields. First, the server sends the client a client configuration endpoint URI in the registration_client_uri field. This URI provides all of the management functionality for this specific client. The client uses this URI directly as it’s given, with no additional parameters or transformations required. It’s often unique for each client registered at an authorization server, but it’s entirely up to the authorization server to decide how the URI itself is structured. Second, the server also sends a specialized access token, called a registration access token, in the registration_access_token field. This is an OAuth bearer token that the client can use to access the client configuration endpoint, and nowhere else. As with all other OAuth tokens, it’s entirely up to the authorization server what the format of this token is, and the client uses it as given.
Let’s take a look at a concrete example. First, the client sends the same registration request to the registration endpoint that was made in the example in section 12.1.3. The server responds in the same way, except that the JSON object is extended as we discussed. Our authorization server creates the client configuration endpoint URI by concatenating the client ID to the registration endpoint, in keeping with common RESTful design principles, but the authorization server is free to format this URL however it pleases. The registration access token in our server is another randomized string like other tokens we generate.
HTTP/1.1 201 Created Content-Type: application/json { "client_id": "1234-wejeg-0392", "client_secret": "6trfvbnklp0987trew2345tgvcxcvbjkiou87y6t5r", "client_id_issued_at": 2893256800, "client_secret_expires_at": 0, "token_endpoint_auth_method": "client_secret_basic", "client_name": "OAuth Client", "redirect_uris": ["http://localhost:9000/callback"], "client_uri": "http://localhost:9000/", "grant_types": ["authorization_code"], "response_types": ["code"], "scope": "foo bar baz", "registration_client_uri": "http://localhost:9001/register/1234-wejeg-0392" "registration_access_token": "ogh238fj2f0zFaj38dA" }
The remainder of the registration response is the same as it was earlier. If the client wants to read its registration information, it sends an HTTP GET request to the client configuration endpoint, using the registration access token in the authorization header.
GET /register/1234-wejeg-0392 HTTP/1.1 Accept: application/json Authorization: Bearer ogh238fj2f0zFaj38dA
The authorization server checks to make sure that the client referred to in the configuration endpoint URI is the same one that the registration access token was issued to. As long as everything is valid, the server responds similarly to a normal registration request. The body is still a JSON object describing the registered client, but the response code is now an HTTP 200 OK message instead. The authorization server is free to update any of the client’s fields, including the client secret and registration access token, but the client ID doesn’t change. In this example, the server has rotated the client’s secret for a new one, but all other values remain the same. Note that the response includes the client configuration endpoint URI as well as the registration access token.
HTTP/1.1 200 OK Content-Type: application/json { "client_id": "1234-wejeg-0392", "client_secret": "6trfvbnklp0987trew2345tgvcxcvbjkiou87y6", "client_id_issued_at": 2893256800, "client_secret_expires_at": 0, "token_endpoint_auth_method": "client_secret_basic", "client_name": "OAuth Client", "redirect_uris": ["http://localhost:9000/callback"], "client_uri": "http://localhost:9000/", "grant_types": ["authorization_code"], "response_types": ["code"], "scope": "foo bar baz", "registration_client_uri": "http://localhost:9001/register/1234-wejeg-0392" "registration_access_token": "ogh238fj2f0zFaj38dA" }
If the client wants to be able to update its registration, it sends an HTTP PUT request to the configuration endpoint, again using the registration access token in the authorization header. The client includes its entire configuration as returned from its registration request, including its previously issued client ID and client secret. However, just as in the initial dynamic registration request, the client cannot choose its own values for its client ID or client secret. The client also doesn’t include the following fields (or their associated values) in its request:
All other values in the request object are requests to replace existing values in the client’s registration. Fields that are left out of the request are interpreted to be deletions of existing values.
PUT /register/1234-wejeg-0392 HTTP/1.1 Host: localhost:9001 Content-Type: application/json Accept: application/json Authorization: Bearer ogh238fj2f0zFaj38dA { "client_id": "1234-wejeg-0392", "client_secret": "6trfvbnklp0987trew2345tgvcxcvbjkiou87y6", "client_name": "OAuth Client, Revisited", "redirect_uris": ["http://localhost:9000/callback"], "client_uri": "http://localhost:9000/", "grant_types": ["authorization_code"], "scope": "foo bar baz" }
The authorization server once again checks to make sure that the client referred to in the configuration endpoint URI is the same one that the registration access token was issued to. The authorization server will also check the client secret, if included, to make sure that it matches the expected value. The authorization server responds with a message identical to the one from a read request, an HTTP 200 OK with the details of the registered client in a JSON object in the body. The authorization server is free to reject or replace any input from the client as it sees fit, as with the initial registration request. The authorization server is once again able to change any of the client’s metadata information, except for its client ID.
If the client wants to unregister itself from the authorization server, it sends an HTTP DELETE request to the client configuration endpoint with the registration access token in the authorization header.
DELETE /register/1234-wejeg-0392 HTTP/1.1 Host: localhost:9001 Authorization: Bearer ogh238fj2f0zFaj38dA
The authorization server yet again checks to make sure that the client referred to in the configuration endpoint URI is the same one that the registration access token was issued to. If they match, and if the server is able to delete the client, it responds with an empty HTTP 204 No Content message.
HTTP/1.1 204 No Content
From there, the client needs to discard its registration information including its client ID, client secret, and registration access token. The authorization server should also, if possible, delete all access and refresh tokens associated with the now-deleted client.
Now that we know what’s expected for each action, we’re going to implement the management API in our authorization server. Open up ch-12-ex-2 and edit the authorizationServer.js file for this exercise. We’ve provided an implementation of the core dynamic client registration protocol already, so we’ll be focusing on the new functionality needed to support the management protocol. Remember, if you want, you can view all registered clients by visiting the authorization server’s homepage at http://localhost:9001/ where it will print out all client information for all registered clients (figure 12.3).
In the registration handler function, the first thing you may notice is that we’ve abstracted out the client metadata checks from the exercise in section 12.1 into a utility function. This was done so that we could reuse the same checks in several functions. If the requested metadata passes all checks, it’s returned. If any of the checks fails, the utility function sends the appropriate error response over the HTTP channel and returns null, leaving the calling function to return immediately with no further handling required. In the registration function, when we call this check, we now do this:
var reg = checkClientMetadata(req); if (!reg) { return; }
First, we’ll need to augment the client information that’s returned from the registration endpoint. Right after we generate the client ID and secret, but before we render the output in the response, we need to create a registration access token and attach it to the client object to be checked later. We’ll also need to generate and return the client configuration endpoint URI, which in our server will be made by appending the client ID to the registration endpoint’s URI.
reg.client_id = randomstring.generate(); if (__.contains(['client_secret_basic', 'client_secret_post']), reg.token_endpoint_auth_method) { reg.client_secret = randomstring.generate(); } reg.client_id_created_at = Math.floor(Date.now() / 1000); reg.client_secret_expires_at = 0; reg.registration_access_token = randomString.generate(); reg.registration_client_uri = 'http://localhost:9001/register/' + reg.client_id; clients.push(reg); res.status(201).json(reg); return;
Now both the stored client information and the returned JSON object contain the access token and the client registration URI. Next, because we’re going to need to check the registration access token against every request to the management API, we’re going to create a filter function to handle that common code. Remember that this filter function takes in a third parameter, next, which is the function to call after the filter has run successfully.
var authorizeConfigurationEndpointRequest = function(req, res, next) { };
First, we’ll take the client ID off the incoming request URL and try to look up the client. If we can’t find it, return an error and bail.
var clientId = req.params.clientId; var client = getClient(clientId); if (!client) { res.status(404).end(); return; }
Next, parse the registration access token from the request. Although we’re free to use any valid methods for bearer token presentation here, we’re going to limit things to the authorization header for simplicity. As we do in the protected resource, check the authorization header and look for the bearer token. If we don’t find it, return an error.
Finally, if we do get an access token, we need to make sure it’s the right token for this registered client. If it matches, we can continue to the next function in the handler chain. Since we’ve already looked up the client, we attach it to the request so we don’t have to look it up again. If the token doesn’t match, we return an error.
if (regToken == client.registration_access_token) { req.client = client; next(); return; } else { res.status(403).end(); return; }
Now we can start to tackle the functionality. First, we’ll set up handlers for all three functions, making sure to add the filter function to the handler setup. Each of these sets up the special :clientId path element, which is parsed by the Express.js framework and handed to us in the req.params.clientId variable, as used in the previous filter function.
app.get('/register/:clientId', authorizeConfigurationEndpointRequest, function(req, res) { }); app.put('/register/:clientId', authorizeConfigurationEndpointRequest, function(req, res) { }); app.delete('/register/:clientId', authorizeConfigurationEndpointRequest, function(req, res) { });
Let’s start with the read function first. Since the filter function has already validated the registration access token and loaded the client for us, all we have to do is return the client as a JSON object. If we wanted to, we could update the client secret and registration access token before returning the client’s information, but that’s left as an exercise for the reader.
app.get('/register/:clientId', authorizeConfigurationEndpointRequest, function(req, res) { res.status(200).json(req.client); return; });
Next we’ll handle the update function. First check to make sure the client ID and client secret (if supplied) match what’s already stored in the client.
if (req.body.client_id != req.client.client_id) { res.status(400).json({error: 'invalid_client_metadata'}); return; } if (req.body.client_secret && req.body.client_secret != req.client.client_secret) { res.status(400).json({error: 'invalid_client_metadata'}); }
Then we need to validate the rest of the incoming client metadata. We’re going to use the same client metadata validation function as in the registration step. This function will filter out any input fields that aren’t supposed to be there, such as registration_client_uri and registration_access_token.
var reg = checkClientMetadata(req, res); if (!reg) { return; }
Finally, copy over the values from the requested object into our saved client and return it. Since we’re using a simple in-memory storage mechanism, we don’t have to copy the client back into the data store, but a database-backed system may have such requirements. The values in reg are internally consistent and will directly replace anything in client, and if they’re omitted they will overwrite the values in client.
__.each(reg, function(value, key, list) { req.client[key] = reg[key]; });
Once that copy is completed, we can return the client object in the same manner as in the read function.
res.status(200).json(req.client); return;
For the delete function, we have to remove the client from our data storage. We’re going to do this using a couple of library functions from the Underscore.js library to help us out.
clients = __.reject(clients, __.matches({client_id: req.client.client_id}));
We’re also going to do our due diligence as an authorization server and immediately revoke all outstanding tokens, whether they’re access tokens or refresh tokens, that were issued to this client before we return.
nosql.remove(function(token) { if (token.client_id == req.client.client_id) { return true; } }, function(err, count) { console.log("Removed %s clients", count); }); res.status(204).end(); return;
With these small additions, the authorization server now supports the full dynamic client registration management protocol, giving dynamic clients the ability to manage their full lifecycle.
Now we’re going to modify our client to call these functions, so edit client.js. Loading up the client and fetching a token displays an extra set of controls on the client’s homepage (figure 12.4).
Let’s wire up some functionality to those shiny new buttons. First, to read the client data, we’re going to make a simple GET call to the client’s configuration management endpoint and authenticate using the registration access token. We’ll save the results of the call as our new client object, in case something changed, and display them using our protected resource viewer template to show the raw content coming back from the server.
app.get('/read_client', function(req, res) { var headers = { 'Accept': 'application/json', 'Authorization': 'Bearer ' + client.registration_access_token }; var regRes = request('GET', client.registration_client_uri, { headers: headers }); if (regRes.statusCode == 200) { client = JSON.parse(regRes.getBody()); res.render('data', {resource: clien}); return; } else { res.render('error', {error: 'Unable to read client ' + regRes.statusCode}); return; } });
Next we’ll handle the form that allows us to update the client’s display name. We need to make a clone of the client object and delete out the extraneous registration fields, as discussed previously, and replace the name. We’ll send this new object to the client configuration endpoint in an HTTP PUT along with the registration access token. When we get a positive response from the server, we’ll save that result as our new client object and go back to the index page.
app.post('/update_client', function(req, res) { var headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer ' + client.registration_access_token }; var reg = __.clone(client); delete reg['client_id_issued_at']; delete reg['client_secret_expires_at']; delete reg['registration_client_uri']; delete reg['registration_access_token']; reg.client_name = req.body.client_name; var regRes = request('PUT', client.registration_client_uri, { body: JSON.stringify(reg), headers: headers }); if (regRes.statusCode == 200) { client = JSON.parse(regRes.getBody()); res.render('index', {access_token: access_token, refresh_token: refresh_token, scope: scope, client: client}); return; } else { res.render('error', {error: 'Unable to update client ' + regRes.statusCode}); return; } });
Finally, we’ll handle deleting the client. This is a simple DELETE to the client configuration endpoint, once again including the registration access token for authorization. Whatever result we get back, we throw away our client information because, from our perspective, we did our best to unregister the client, whether the server was able to do so or not.
app.get('/unregister_client', function(req, res) { var headers = { 'Authorization': 'Bearer ' + client.registration_access_token }; var regRes = request('DELETE', client.registration_client_uri, { headers: headers }); client = {}; if (regRes.statusCode == 204) { res.render('index', {access_token: access_token, refresh_token: refresh_token, scope: scope, client: client}); return; } else { res.render('error', {error: 'Unable to delete client ' + regRes.statusCode}); return; } });
With these in place, we’ve got a fully managed, dynamically registered OAuth client. Advanced client handling, including editing other fields, rotation of client secrets, and registration access tokens, is left as an exercise to the reader.
Dynamic client registration is a powerful extension to the OAuth protocol ecosystem.
Now that you know how to introduce clients to authorization servers programmatically, let’s take a look at one common application of OAuth: end-user authentication.