## 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 } ----------------------------------------------------------- ```