## Agents
There are in total 3 agents which are nothing but services.
- User $\mathbb{U}_i$ working on App $\mathbb{A}$
- A bridge service $\mathbb{B}$
- User Identity and Data services $\mathbb{C}_1, \mathbb{C}_2, .., \mathbb{C}_n$
$\mathbb{C}_i$ is an identity and data service for some subset of users, which holds these users' personal data in their servers. e.g. user posts at Facebook. There could be many such users on App $\mathbb{A}$ and each such user, say $\mathbb{U}_i$, could have their personal user data available at only **one of the data servers**, say $\mathbb{C_i}$.
## Problem background
User $\mathbb{U}_i$ on app $\mathbb{A}$ would like to access their personal data available in service $\mathbb{C}_i$. One good way for this user to hit the authorize flow for $\mathbb{C}_i$ and then call their user profile APIs. However all $\mathbb{C_i}$ do not necessarily have a consistent API format. Thus $\mathbb{A}$ would need to write separate implementations (authorize, fetch various user data, etc.) for every $\mathbb{C}_i$ to support all types of users. Alternatively, every service $\mathbb{C}_i$ would need to support a standard flavour of the authorize and data fetch routine. None of these may be easy to do.
Enter bridge service $\mathbb{B}$. $\mathbb{B}$ is in reality a data information company and therefore it has integrated with all the $\mathbb{C}_i$ and they also have all the user data available at their respective servers.
So instead of creating separate implementations for each service $\mathbb{C}_i$, $\mathbb{A}$ could only create one connection implementation with $\mathbb{B}$ and ask them for the user data. But $\mathbb{B}$ does not know who the user is or which $\mathbb{C}_i$ the user belong to or if the user actually want to share their data. The problem then becomes what should $\mathbb{B}$ do in order to support these different types of users on different services.
## Problem details
For simplicity, let's assume all the services $\mathbb{C}_i$ support OAuth 2.0 basic flow and also OIDC.
So now $\mathbb{B}$ in agreement with $\mathbb{A}$ and all the $\mathbb{C}_i$ could decide to
- use $\mathbb{C}_i$ identity management to authenticate the user $\mathbb{U}_i$ and input their consent to share the data with App $\mathbb{A}$
- and then once they receive the consent, use their own copy of the data to respond to user request on $\mathbb{A}$
One possible way to do this is to do mirroring. Basically $\mathbb{B}$ could expose OAuth token flow with $\mathbb{A}$ and internally validate $\mathbb{U}_i$'s request via redirections with OAuth token flow of $\mathbb{C}_i$. The detailed request flow would look something like this
1. $\mathbb{U}_i$ initiates a request with $\mathbb{B}$ to get certain data, let's say $\lambda$. They pass along some information which helps $\mathbb{B}$ identify which identity server $\mathbb{C}_i$ is the right server to forward the request to.
2. $\mathbb{B}$, if not having the right consent information, redirects the user to the right $\mathbb{C}_i$'s OAuth 2.0 password flow and gets back on proper consent the authorization code from $\mathbb{C}_i$ and uses token exchange protocol to get the access token and optionally refresh token back from $\mathbb{C}_i$. (Alternatively this could have been compressed using implicit grant or only using OIDC authentication, but they have their own pros and cons and nuances and implications)
3. If $\mathbb{B}$ has these tokens already, they could do one or more of
- call refresh to ensure the tokens are fresh
- check that user access token is valid and there is no revoke called on $\mathbb{C}_i$
4. Once $\mathbb{B}$ receives and verified the tokens, it then
- saves the tokens in a secured DB
- creates a "mirrored" token (implicit grant type) with mirrored expiration
- send this to $\mathbb{A}$ along with the data $\lambda$.
5. In the future $\mathbb{A}$ uses the same "mirrored" token to get the same or other type of data for $\mathbb{U}_i$. If the underlying "real" tokens of $\mathbb{U}_i$ (in $\mathbb{C}_i$) are valid a.k.a Step 3, then the data is returned by $\mathbb{B}$ directly from their servers. If the tokens are not valid, then $\mathbb{B}$ also expires $\mathbb{U}_i$'s "mirrored" token and re-triggers back flow starting from Step 2.
Some obvious logical questions comes to mind.
- How does $\mathbb{B}$ continue to validate $\mathbb{U}_i$'s consent i.e. token expiration across multiple requests. I mean is it OK to cache this data for some time?
- Does $\mathbb{B}$ need to implement OAuth themselves or could use another token based authorization?
- Would it be ok do just do an authentication (OIDC) and assume authorization defaulting to some fixed values, e.g. user_profile_view, etc.?
But there are also some assumption, potential limitations with the way identity management is implemented by the different services $\mathbb{C}_i$. Some assumptions are
- The root assumptions (base case) is that OAuth 2.0 is supported by these services. Or only those services which support OAuth will be supported via this bridge mechanism.
- `password flow` may not always be the most appealing mode of authorization. If $\mathbb{C}_i$ does not support `passwordless`, what could be done? Could $\mathbb{B}$ collaborate with different $\mathbb{C}_i$ and help them configure `passwordless` especially if they use known identity management providers like onelogin? If not, what are the possible solutions?
## The Passwordless problem details
The regularly used `password flow` may not always be the preferred mode of authorization since there is a high chance that $\mathbb{C}_i$ is not regularly accessed or maybe user registration is pending. But $\mathbb{C}_i$ themselves may not have `passwordless` flow embedded in their oauth solutioning.
E.g. It is possible that $\mathbb{C}_i$ has built their authorization using an IAM provider like `Auth0` and therefore are able to support `passwordless` flow such as OTP or WebAuthN by just some config changes to their login flow. But another service $\mathbb{C}_j$ may be using their own identity provider or another provider which does not support `passwordless` out of the box.
If $\mathbb{B}$ still somehow wanted to provide this delegated authorization service to User $\mathbb{U}_i$ on App $\mathbb{A}$ anyways, how could this be done in a secure and industry recommended way?
## The specification
In this section we try to emulate the API flow between various parties in a diagrammatic way.
### Passwordless
Below is the high level flow of packets in case of passwordless authorization.
<pre>
[device] [device-server] [bridge-svc]
| | |
| (get-pat) | |
[get-data] |---------(i)-------->| |
|<-------r(i)---------| |
| | |
| | |
|-------------------(ii)------------------->|
| (authorize) |
| |<-------(ii.a)-------|
| | (get-pubk) |
| |-------r(ii.a)------>|
|<-----------------r(ii)--------------------|
| | |
| | |
|-------------------(iii)------------------>|
| (send-factors) |
|<-----------------r(iii)-------------------|
| | |
| | |
|-------------------(iv)------------------->|
| (validate-otp) |
|<-----------------r(iv)--------------------|
| (auth-code) |
| | |
| | |
|-------------------(iv)------------------->|
| (xchg-w-token) |
|<-----------------r(iv)--------------------|
| res: token |
| | |
| | |
[device] [device-server] [bridge-svc]
</pre>
The requests and their corresponding responses are of the following kind (assuming all communication over mutual TLS v1.3) and all redirect URIs are validated based on some set consensus.
1. Call __(i)__ (get-pat) is made to fetch device's personal
access token available with the device backend
```
-----------------------------------------------------------
GET /backend/device/{id}/get-pat -h"requestID:**;CSRFToken:**" -d
{
"token": **,
"secret": **
}
-----------------------------------------------------------
RESP {"pat": **}
-----------------------------------------------------------
```
2. Call __(ii)__ [authorize] initiates the authorize call on the
bridge service. It send to the service, the personal access
token along with a bunch of user attributes to help the
bridge service identify the user. Once this user is
identified and the personal access token is verified then
the relevant passwordless factors are conveyed to device.
```
-----------------------------------------------------------
GET /bridge/oauth/authorize -h"requestID:**;CSRFToken:**" -d
{
"uuid": [
{"field1": "value1"},
{"field2": "value2"}
],
"device_id": **,
"pat": "personal token",
"scope": "SCOPE",
"response_type": "code",
"client_id": "{yourClientId}",
"redirect_uri": "{https://yourApp/callback}",
"code_challenge": "CODE_CHALLENGE",
"code_challenge_method": "S256"
}
-----------------------------------------------------------
RESP
{
"factors": [
{"id": **, mode": **, "value": **},
{"id": **, "mode": **, "value": **}
]
}
-----------------------------------------------------------
```
3. Call __(ii.a)__ (get-pubk) is used to stream the public key
associated with the device (sent in __(ii)__) to the bridge
service. The bridge service can then check whether the
key-pair (pat & pubk) is a valid crypto pair and only on
a validation will send the factors in the get-factors call.
```
-----------------------------------------------------------
GET /backend/device/get-pubk -h"requestID:**;CSRFToken:**" \
-d {"device_id": **}
-----------------------------------------------------------
RESP {"v": "**"}
-----------------------------------------------------------
```
4. Call __(iii)__ (send-factors) is used to send to the
bridge service the choices of factors over which the OTP
should be sent.
```
-----------------------------------------------------------
GET /bridge/device/send-factor -h"requestID:**;CSRFToken:**" \
-d {"factors": [{"id": **}, {"id": **}]}
-----------------------------------------------------------
RESP [{"id": **, "status": **}, {"id": **, "status": **}]
-----------------------------------------------------------
```
5. Call __(iv)__ (validate-otp) is used to validate the
latest OTP associated with the requestID.
```
-----------------------------------------------------------
GET /bridge/device/validate-otp -h"requestID:**;CSRFToken:**" \
-d {"valhash": **}
-----------------------------------------------------------
RESP {"message": **, "authCode": **}
-----------------------------------------------------------
```
6. Call __(v)__ (xchg-w-token) is used to ask to exchange
the authorization code received in the previous call with
the access token and refresh token pairs (a.k.a oauth 2.0)
```
-----------------------------------------------------------
GET /bridge/device/xchg-w-token -h"requestID:**;CSRFToken:**" \
-d
{
"clientId": **,
"clientSecret": **,
"authCode": **,
"grantType": **,
"redirectURI": **
}
-----------------------------------------------------------
RESP
{
"accessToken": **,
"expires": **,
"idToken": **,
"refreshToken": **,
"scope": **,
}
-----------------------------------------------------------
```
### Password approach
Below is the high level flow of packets in case of oauth delegated redirection approach, as explained above.
<pre>
[device] [device-server] [bridge-svc] [utility-svc]
| | | |
| (get-pat) | | |
[get-data] |--------(i)------->| | |
|<------r(i)--------| | |
| | | |
| | | |
|-----------------(ii)----------------->| |
| (authorize) | |
| |<------(ii.a)------| |
| | (get-pubk) | |
| |------r(ii.a)----->| |
| | | |
| | |-------(ii.b)----->|
| | | (authorize) |
| | |<-----r(ii.b)------|
| | |-------(ii.c)----->|
| | | (xchg-w-token) |
|<---------------r(ii)------------------| |
| res: auth code | |
| | |<-----r(ii.c)------|
|---------------- (iii)---------------->| |
| (xchg-w-token) | |
|<---------------r(iv)------------------| |
| res: token | |
| | | |
| | | |
[device] [device-server] [bridge-svc] [utility-svc]
</pre>
The requests and their corresponding responses are of the following kind (assuming all communication over mutual TLS v1.3) and all redirect URIs are validated based on some set consensus. Also all communications assume PKCE grant type (or a more secure but customized flavor using PAT exchange). Also we could use nonce value to create a state token to mitigates CSRF based attacks.
1. Call __(i)__ (get-pat) is made to fetch device's personal
access token available with the device backend
```
-----------------------------------------------------------
GET /backend/device/{id}/get-pat -h"requestID:**;CSRFToken:**" -d
{
"token": **,
"secret": **
}
-----------------------------------------------------------
RESP {"pat": **}
-----------------------------------------------------------
```
2. Call __(ii)__ [authorize] initiates the authorize call on the bridge
service. It send to the service, the personal access token along with a bunch
of user attributes to help the bridge service identify the user. Once this user
is identified and the personal access token is verified then the request is
redirected to the utility's IAM service. One important thing to note is that
the redirection awaits the success of the PAT validation check once the response
to __ii.a__ is received. If not a success, then we return authorization error.
```
-----------------------------------------------------------
GET /bridge/oauth/authorize -h"requestID:**;CSRFToken:**" -d
{
"uuid": [
{"field1": "value1"},
{"field2": "value2"}
],
"device_id": **,
"pat": "personal token",
"scope": "SCOPE",
"response_type": "code",
"client_id": "{device-client-id}",
"redirect_uri": "{https://device/callback}",
"code_challenge": "CODE_CHALLENGE_1",
"code_challenge_method": "S256"
}
-----------------------------------------------------------
RESP
HTTP/1.1 302 Found
Location: {https://utility/authorize}?redirect_uri=\
"{https://bridge/oauth/callback}"&..
(for details see ii.b)
-----------------------------------------------------------
```
3. Call __(ii.a)__ (get-pubk) is used to stream the public key associated with
the device (sent in __(ii)__) to the bridge service. The bridge service can
then check whether the key-pair (pat & pubk) is a valid crypto pair and only on
a validation will redirect the request to the utility's authorize endpoint.
```
-----------------------------------------------------------
GET /backend/device/get-pubk -h"requestID:**;CSRFToken:**" \
-d {"device_id": **}
-----------------------------------------------------------
RESP {"v": "**"}
-----------------------------------------------------------
```
4. Call __(ii.b)__ (authorize) is the utility's version of the authorize
endpoint, which is the true account of the identity management service
for a utility user. All the parameters are different than request __ii__,
except for the requestID header (or some parameter which helps trace the call
to the origin). On a side note, the code expires after one time use and redirect URI goes through the requisite validation as suggested in the three way specs.
```
-----------------------------------------------------------
GET /utility/oauth/authorize -h"requestID:**;CSRFToken:**" -d
{
"scope": "SCOPE",
"response_type": "code",
"client_id": "{bridge-client-id}",
"redirect_uri": "{https://bridge/oauth/callback}",
"code_challenge": "CODE_CHALLENGE_2",
"code_challenge_method": "S256"
}
-----------------------------------------------------------
RESP
HTTP/1.1 302 Found
Location: https://.../bridge/oauth/callback?code=\
{BRIDGE_AUTHORIZATION_CODE}
-----------------------------------------------------------
```
5. a> The bridge service's callback handler's responsibility is to record this
event and the auth code, generate a new auth code mirroring the event and
redirecting the request to the device. Parallely, it will also initiate the
token exchange with the utility.
```
HTTP/1.1 302 Found
Location: https://.../device/oauth/callback?code=\
{DEVICE_AUTHORIZATION_CODE}
-----------------------------------------------------------
```
5. b> Parallely a call __(ii.c)__ (xchg-w-token) is made to
request for token in exchange for authorization code with
the utility. This exchanged token is then retained with the
bridge service only and the event is recorded so that any
impending device request for token exchange can be awaited.
```
-----------------------------------------------------------
POST /utility/oauth/token
{
"grant_type": "authorization_code",
"client_id": "{bridge-client-id}",
"client_secret": "{bridge-client-secret}",
"code": "{AUTHORIZATION_CODE}
}
-----------------------------------------------------------
RESP
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token":"eyJz93a...k4laUWw",
"refresh_token":"GEbRxBN...edjnXbL",
"id_token":"eyJ0XAi...4faeEoQ",
"token_type":"Bearer",
"expires_in":86400
}
-----------------------------------------------------------
```
6. Call __(iii)__ (xchg-w-token) is used to request for token
in exchange for authorization code with the bridge service.
This call awaits the success status of __ii.c__ with certain
timeout. IF successful, a token pair is created a.k.a oauth2.
The token pair will be completely separate from the one
received in __ii.c__ but it mirrors the scope and validity
with it.
```
-----------------------------------------------------------
POST /bridge/oauth/token
{
"grant_type": "authorization_code",
"client_id": "{device-client-id}",
"code": "{DEVICE_AUTHORIZATION_CODE}
}
-----------------------------------------------------------
RESP
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token":"cFy2zZ67a...h78lGRy",
"refresh_token":"KI5cYsPK...f3kbZaT",
"id_token":"cxH4YZy...6qlpH4R",
"token_type":"Bearer",
"expires_in":86400
}
-----------------------------------------------------------
```