352 lines
15 KiB
Markdown
352 lines
15 KiB
Markdown
# OAuth3 - A SPEC we can Auth*x* with
|
|
All endpoints defined. All data described. All setup process automated.
|
|
|
|
The goal of OAuth3 is to establish a well-defined specification for Authentication and Authorization that gives the identity owner (i.e. the human that uses the Internet) the choice of where to store their identity (i.e. not just Facebook) and also gives the developer the ability to write code once that can work with *any* service that adheres to the spec (i.e. not just a limited pre-programmed list of Facebook, Twitter, and Google).
|
|
|
|
The specification was created with the intent that an entry level developer can copy a few files over to a static file server and copy a few lines of code into a web, mobile, or desktop application and have secure authentication in that application.
|
|
|
|
The ultimate goal of OAuth3 is to re-federate the Internet so that content creators (here's lookin at you "Insta" and "snap" celebs) have the choice of where to store their data and which services to use - whether on "the cloud" (large centralized systems) or on "home cloud" systems.
|
|
|
|
## Why not OAuth2?
|
|
|
|
OAuth2 started to solve an important problem, but it just didn't get there.
|
|
|
|
### OAuth2 can't be implemented.
|
|
|
|
OAuth2 isn't a protocol, it's "a framework for a protocol"
|
|
|
|
That sounds like nonsense, but it's important nonsense. A good specification should result in two or more people being able to read the specification, implement it, and without having communicated to each other at all, both of their implementations should be compatible - anyone who writes an application for one should find that it works for the other with 0 changes to the application.
|
|
|
|
This is not the case with OAuth2.
|
|
|
|
* The endpoints are not defined (i.e. /authorization_dialog vs /auth_diag vs /login)
|
|
* The doesn't define data format (i.e. urlencoded vs XML vs JSON)
|
|
* It does define internal implementation details (i.e. how the service should handle state)
|
|
|
|
## Why not OIDC?
|
|
|
|
OpenID Connect is an incremental improvement upon OAuth2. It is a step in the right direction,
|
|
but it too stops far short of the goals we have set.
|
|
|
|
### OIDC is built for enterpise, not for end-users
|
|
|
|
OAuth3 was in development simultaneously with OIDC and is being released long after OIDC,
|
|
as such many of the conventions of OAuth3 have been changed to match those of OIDC where
|
|
appropriate. However, they are still separate specifications with different goals.
|
|
|
|
Some of the failings of OAuth2 we see left unsolved in OIDC are that it
|
|
|
|
* focuses primarily on service providers, not identity and content owners
|
|
* cannot be made backwards compatible with OAuth2
|
|
* requires manual intervention by a developer to register an application and accept agreements
|
|
* requires manual intervention by users to accept License and TOS agreements
|
|
|
|
## Why OAuth3?
|
|
|
|
A protocol to enable machines to help people protect their identity - not just in "the cloud",
|
|
but also in the home.
|
|
|
|
### OAuth3 gives humans choice, works in the IoT-enabled home
|
|
|
|
* a single implementation is sufficient for all services and domains
|
|
* can be used in apps and on devices in the home
|
|
* all endpoints well-defined
|
|
* automated registration
|
|
* automated License and ToS acceptence
|
|
* more capability on the client means greater security and less server load
|
|
* *can* store identity on client devices or in "the cloud"
|
|
* does not *require* an application server (though that sure makes Interneting convenient)
|
|
* human-friendly
|
|
* machine-friendly
|
|
* no developer required
|
|
# Implementation Details
|
|
OAuth3 is intended to work in URL-based flows such as `https://`, `app://`, and (ostensibly) `dat://`.
|
|
|
|
### subject, issuer, authorized party, and audience
|
|
|
|
Just a quick intro to vocabulary that will be explained more thoroughly later on:
|
|
|
|
* **subject** (**sub**)- typically a username or email address that references the real, live person that we care about
|
|
* **issuer** (**iss**) - the service or device that issues tokens (or manages public keys). In the old-world model this is Facebook, Twitter, etc
|
|
* **audience** (**aud**) - the service or device that exchanges tokens for goods or services. In the old-world model this is Facebook, Twitter, etc
|
|
* **authorized party** (**azp**) - the service or device that uses tokens to accomplish the subject's goals. In the old-world model this is a site that has a "Login with Facebook" button
|
|
|
|
### Scopes
|
|
|
|
Scopes are federated schemas. Any issuer may act as the agent of the subject to authorize any azp for any audience. To make this possible scopes are defined as `<<schema>>@<<domain.tld>>` (i.e. dns@oauth3.org) and can be discovered as `https://aud.tld/.well-known/scopes@oauth3.org/<<schema>>@<<domain.tld>>.json`.
|
|
|
|
Examples of well-known scope-schemas:
|
|
|
|
* authn@oauth3.org
|
|
* domains@oauth3.org
|
|
* dns@oauth3.org
|
|
|
|
## There are 4 potential Identity Delegation styles
|
|
|
|
The following are just sample UX to show the difference in the styles. Typically, a single UX can encompass all 4 styles.
|
|
|
|
![](https://media.daplie.com/oauth3/oauth3-flows-800x800.png)
|
|
|
|
### Type 1: Full Delegation (no discovery)
|
|
|
|
This provides the simplest user experience by hiding all choices from the user and presenting only a single login button.
|
|
|
|
In this scenario a default identity issuer has already been selected (presumably oauth3.org) and the single button is immediately active.
|
|
|
|
The advanced button presents another input box that can be used to enter an address.
|
|
|
|
Why you would want this: If you want to have as little code as possible and you don't care that the user may have to perform an additional step.
|
|
|
|
### Type 2: Address Flow (email - no discovery)
|
|
|
|
In this flow a default identity issuer has also already been selected and the user provides their email address which will be passed to the issuer as the subject.
|
|
|
|
The advanced button presents another input box that can be used to manually specify an identity issuer (requires discovery).
|
|
|
|
Why you would want this: If you want to collect the user's email address up front without waiting to request permission from the issuer/audience.
|
|
|
|
### Type 3: [OPTIMAL] Address Flow (oauth3 - discovery)
|
|
|
|
In this flow the identity issuer will be chosen based on the address provided by the user. If the address is discovered to be an oauth3 address it will be used as such. Otherwise the flow will fallback to type 2.
|
|
|
|
**email address**: this is the same as flow type 2
|
|
|
|
**oauth3 address**: the identity issuer specified will be used, the username is optional
|
|
|
|
Why you would want this: You want your users to enjoy the most choice with the least friction.
|
|
|
|
### Type 4: Advanced Flow (manual + discovery)
|
|
|
|
There is a chance that someone would like to use an oauth3 subject address (i.e. jane@smithfam.net) with a different identity issuer than the one identified by the subject.
|
|
|
|
In this flow the subject and the identity issuer can be manually selected independently, ignoring any discovery of the subject.
|
|
|
|
Why you would want this: You want to support every possible OAuth3 use case.
|
|
|
|
## OAuth3 Issuer Discovery Endpoint and Process
|
|
|
|
Any site (**iss**) which supports OAuth3 (oauth3.org, for example) can be discovered automatically and all configuration details can be taken care of without human intervention.
|
|
|
|
To be compatible the site MUST provide a static json file (`directives.json`) containing all of the directives and endpoints required to complete the OAuth3 processes.
|
|
|
|
It must also provide an `index.html` which can be used to pass the contents of `directives.json` via url parameters so that OAuth3 can work on simple static servers that do not have CORS support.
|
|
|
|
|
|
```
|
|
https://issuer.tld/.well-known/oauth3/directives.json
|
|
https://issuer.tld/.well-known/oauth3/ (index.html)
|
|
```
|
|
|
|
The site receiving the url parameters (**azp**) must have its own `directives.json` which must specify `/.well-known/oauth3/callback.html` as the callback frame through which it receives
|
|
|
|
```
|
|
https://azp.tld/.well-known/oauth3/callback.html
|
|
```
|
|
|
|
### Native Flow
|
|
|
|
**Step 1**
|
|
|
|
The subject types their subject address, such as jane@smithfam.net, into an input field
|
|
|
|
**Step 2**
|
|
|
|
A native application should simply request `https://smithfam.net/.well-known/oauth3/directives.json`, according to our example above.
|
|
|
|
**Step 3**
|
|
|
|
The UI should be updated with the name and icon specified in the issuer directives, if any.
|
|
|
|
### Browser Flow
|
|
|
|
**Step 1**
|
|
|
|
The subject types their subject address, such as jane@smithfam.net, into an input field
|
|
|
|
**Step 2**
|
|
|
|
The azp will discover `smithfam.net` (according to our example in step 1).
|
|
|
|
First it must check if it itself is smithfam.net. If it is, it will retrieve `/.well-known/oauth3/directives.json` directly.
|
|
|
|
Otherwise the azp generates a random state and creates a callback function to receive the response from the subject's local user-agent at `https://smithfam.net/.well-known/oauth3/#/`, bypassing the need to round-trip to the server in most cases. It may be opened in a hidden iframe and must have the appropriate hashquery parameters, including the state.
|
|
|
|
Example **callback function**:
|
|
|
|
```
|
|
var state = OAUTH3.utils.randomState();
|
|
window['--oauth3-callback-' + state] = function (params) {
|
|
// handle response hashquery parameters
|
|
};
|
|
```
|
|
|
|
Example **discovery url**:
|
|
|
|
```
|
|
https://smithfam.net/.well-known/oauth3/#/
|
|
?action=directives
|
|
&state=<<state>>
|
|
&redirect_uri=<<encodeURIComponent("https://azp.tld/.well-known/oauth3/callback.html#/")>>
|
|
&response_type=rpc
|
|
&_method=GET
|
|
&_pathname=<<encodeURIComponent(".well-known/oauth3/directives.json")>>
|
|
&debug=false
|
|
```
|
|
|
|
The debug parameter's behavior is defined by the issuer, but should cause automatic redirects to be replaced with manual intervention (such as clicking a link to continue) and provide information helpful for debugging.
|
|
|
|
**Step 3**
|
|
|
|
The issuer (smithfam.net) will receive the query request for `directives.json` and respond with base64-encoded json to the azp's `callback.html`.
|
|
|
|
The issuer should application cache all files necessary for responding to this request and may also cache the contents of `directives.json` so that no requests are made to the server in the normal case.
|
|
|
|
The `redirect_uri` in our example was specified as `https://azp.tld/.well-known/oauth3/callback.html#/`, so we'll continue to use that.
|
|
|
|
```
|
|
https://azp.tld/.well-known/oauth3/callback.html#/
|
|
?state=<<params.state>>
|
|
&directives=<<OAUTH3._base64.encodeUrlSafe(JSON.stringify(directives))>>
|
|
&debug=<<params.debug>>
|
|
```
|
|
|
|
This url replaces the current window (currently loaded at the discovery url).
|
|
|
|
**Step 4**
|
|
|
|
The hidden frame that the azp had opened has now been redirected to its `callback.html`. It should pass the parameters back to the original program and continue the user flow.
|
|
|
|
Example **callback uri** on the azp:
|
|
|
|
`https://azp.tld/.well-known/oauth3/callback.html`
|
|
```html
|
|
<html>
|
|
<body>
|
|
<script>
|
|
(function () {
|
|
'use strict';
|
|
|
|
|
|
var params = OAUTH3.query.parse(window.location.hash);
|
|
|
|
window.parent['--oauth3-callback-' + params.state](params);
|
|
window.close();
|
|
|
|
|
|
}());
|
|
</script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
**Step 5**
|
|
|
|
The UI should be updated with the name and icon specified in the issuer directives, if any.
|
|
|
|
## OAuth3 Token Issuance, Endpoints, and Process
|
|
|
|
For an issuer such as smithfam.net the directives must specify that the `authorization_dialog` endpoint is `https://smithfam.net/#/authorization_dialog/`
|
|
|
|
**First Token Grant / First Login**
|
|
|
|
Once the issuer has been discovered the azp must store any state about the subject or application and construct a url to place in an anchor tag that will either redirect the window to the issuer directly, or open a popup window by using the `_target` attribute.
|
|
|
|
According to browser policy, `window.open` may not be used asynchronously (no promises or requests), if it is used.
|
|
|
|
**Pre-Authorized Token Grant / Subsequent Logins**
|
|
|
|
The authorization dialog may be opened from a hidden iFrame. If the user is signed in and has already authorized the app a token will be granted. Otherwise an error will be issued.
|
|
|
|
An error would look like this:
|
|
|
|
```
|
|
https://azp.tld/.well-known/oauth3/callback.html#/
|
|
?error=enoauth@oauth3.org
|
|
&error_description=Not already authorized
|
|
&error_uri=https://oauth3.org/errors/enoauth@oauth3.org
|
|
&debug=false
|
|
```
|
|
|
|
### Step 1
|
|
|
|
Example **authorization dialog** url (using the example above):
|
|
|
|
```
|
|
https://smithfam.net/#/authorization_dialog/
|
|
?response_type=token
|
|
&scope=authn@oauth3.org
|
|
&state=<<OAUTH3.utils.randomState()>>
|
|
&client_uri=azp.tld
|
|
&client_id=azp.tld
|
|
&subject=jane@smithfam.net
|
|
&jwk_kty=<<EC>>
|
|
&jwk_kid=<<thumbprint of key>>
|
|
&jwk_XXX=<<property XXX of jwk>>
|
|
&redirect_uri=<<encodeURIComponent('https://azp.tld/.well-known/oauth3/callback.html')>>
|
|
&debug=false
|
|
```
|
|
|
|
NOTE: `redirect_uri` itself may also contain URI-encoded components
|
|
|
|
In OAuth3 `client_uri` replaces `client_id` and so `client_id` is only necessary for OAuth2 backwards compatibility and for applications that need multiple ids or otherwise require manual registration.
|
|
|
|
`scope` is optional. Generally speaking scopes should be defined as part of subsequent authorization, not initial authentication.
|
|
|
|
`subject` is optional, but allows the issuer to skip the step of asking the user for their username / email.
|
|
|
|
TODO It should also be possible to pass qualifiers (acr) for the security requirements of the azp (recency of login, mfa requirements, etc).
|
|
|
|
TODO `jwk`. should be optional. It may be used to publish a public key such that the application may generate its own tokens, if the issuer supports it.
|
|
|
|
|
|
### Step 2 (determine subject / get username/email)
|
|
|
|
The user will be asked for their email or cloud address.
|
|
|
|
It is up to the issuer's implementation to decide, but it is possible that the issuer will delegate to the issuer specified by the address.
|
|
|
|
If `subject` was provided (or the user is logged in), this step will be skipped.
|
|
|
|
### Step 3 (authenticate / login)
|
|
|
|
How authentication is to be done is an implementation detail of the issuer.
|
|
|
|
It is recommended to avoid using passwords and to instead use device-based authentication.
|
|
|
|
In the reference implementation the user is sent an email with a login code or the device generates a new public/private keypair.
|
|
|
|
### step 4 (authorize / grant)
|
|
|
|
A JWT (token) with the following properties will be generated:
|
|
|
|
```
|
|
{ "jti": "<<nonce>>"
|
|
, "iat": <<issued at unix timestamp in seconds>>
|
|
, "amr": "pwd@oauth3.org"
|
|
, "laa": <<last authentication at unix timestamp in seconds>>
|
|
, "exp": <<expiry unix timestamp in seconds>>
|
|
, "sub": "<<psuedonymous pairwise identifier>>"
|
|
, "iss": "smithfam.net"
|
|
, "azp": "azp.tld"
|
|
, "aud": "azp.tld"
|
|
, "scp": "auth@oauth3.org,domains@oauth3.org"
|
|
, "ttl": 0 /* cannot be renewed even before exp */
|
|
}
|
|
```
|
|
|
|
It isn't necessary to have a refresh token because the token can be refreshed through a hidden iframe in a browser in the same way it was acquired the first time.
|
|
|
|
However, where the issuer supports refresh tokens, they must have the additional property of `renew: true`
|
|
|
|
The token will be passed back to the application in the following form:
|
|
|
|
```
|
|
https://azp.tld/.well-known/oauth3/callback.html#/
|
|
?access_token=<<token>>
|
|
&token_type=<<Bearer>>
|
|
&refresh_token=<<refresh_token>>
|
|
&expires_in=<<token.exp - now>>
|
|
&scope=<<token.scp>>
|
|
&state=<<params.state>>
|
|
&debug=false
|
|
``` |