Authentication

We provide an OpenID Connect compliant interface that should work well with any OpenID Connect certified relying party software.

This document will provide a high level overview, but we recommend that users familiarise themselves with the following specs:

Base URL:

Discovery

https://identity.moneyhub.co.uk/oidc/.well-known/openid-configuration

OpenID Connect Discovery Spec

Our discovery document is available here. It will contain our up-to-date machine readable configuration and for example will list our:

  • token endpoint
  • authorization endpoint
  • jwks endpoint
  • scopes that we support
  • claims that we support
  • the cryptographic algorithms we support
  • the response types we support

Examples of discovery metadata from other providers are:

Response Types

Our discovery doc will list the response types that we support. Currently these are: code, code id_token and id_token.

code is fairly straight forward and is the standard OAuth 2 authorization code flow.

code id_token is one of the variants of the hybrid flow and isn’t always understood. At a basic level it means that we will send an id_token along with the authorization code when we redirect the user back to your redirect_uri. This id_token doesn’t contain any identity information, but is rather a detached signature which cryptographically binds the authorization_code we send back with the nonce and the state that you sent to us. It prevents a certain class of code interception attacks and we encourage implementers to use it rather than the basic authorization code flow.

Authorization Endpoint

https://identity.moneyhub.co.uk/oidc/auth

The Authorization Endpoint performs Authentication of the End-User. This is done by sending the User Agent to the Authorization Server's Authorization Endpoint for Authentication and Authorization, using request parameters defined by OAuth 2.0 and additional parameters and parameter values defined by OpenID Connect.

OpenID Connect Authorization Spec

We support the use of request objects and claims parameter at this endpoint.

Below you can see an example auth urls constructed according to the OpenID Spec

// This example assumes the use of an OpenID Client (e.g. Node OpenId client)

const authParams = {
  client_id: "67e5c1ce-1a3b-4ca8-a2d8-49ab822dbc24",
  scope: "openid id:1ffe704d39629a929c8e293880fb449a",
  state: "your-state-value",
  redirect_uri: "http://localhost:3001",
  response_type: "code",
  prompt: "consent",
}

const claims = {
  id_token: {
    sub: {
      essential: true,
      value: "6202948122dd6f18f3c15efb",
    },
    mh:con_id: {
      essential: true,
    },
  },
}

const request = await client.requestObject({
  ...authParams,
  claims,
  max_age: 86400,
})

const url = client.authorizationUrl({
  ...authParams,
  request,
})

/* Example auth url using request object

https://identity.moneyhub.co.uk/oidc/auth?client_id=67e5c1ce-1a3b-4ca8-a2d8
-49ab822dbc24&prompt=consent&redirect_uri=http%3A%2F%2Flocalhost%3A3001&req
uest=eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJhdWQiOiJodHRwczovL2lkZW50aXR5Lm
1vbmV5aHViLmNvLnVrL29pZGMiLCJpYXQiOjE2NDQzMzYyNzYsImV4cCI6MTY0NDMzODA3Niwia
XNzIjoiNjdlNWMxY2UtMWEzYi00Y2E4LWEyZDgtNDlhYjgyMmRiYzI0IiwiY2xpZW50X2lkIjoi
NjdlNWMxY2UtMWEzYi00Y2E4LWEyZDgtNDlhYjgyMmRiYzI0Iiwic2NvcGUiOiJvcGVuaWQgaWQ
6MWZmZTcwNGQzOTYyOWE5MjljOGUyOTM4ODBmYjQ0OWEiLCJzdGF0ZSI6InlvdXItc3RhdGUtdm
FsdWUiLCJyZWRpcmVjdF91cmkiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJjbGFpbXMiOnsia
WRfdG9rZW4iOnsic3ViIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiI2MjAyOTQ4MTIyZGQ2
ZjE4ZjNjMTVlZmIifSwibWg6Y29uX2lkIjp7ImVzc2VudGlhbCI6dHJ1ZX19fSwicmVzcG9uc2V
fdHlwZSI6ImNvZGUiLCJwcm9tcHQiOiJjb25zZW50In0.&response_type=code&scope=open
id%20id%31ffe704d39629a929c8e293880fb449a&state=your-state-value

*/

Token Endpoint

https://identity.moneyhub.co.uk/oidc/token

To obtain an Access Token, an ID Token, and optionally a Refresh Token, the RP (Client) sends a Token Request to the Token Endpoint to obtain a Token Response when using the Authorization Code Flow.

OpenID Connect Token Spec

We support the following grant types:

  • authorization_code
  • client_credentials
  • refresh_token

Client credentials

The client_credentials grant supports 2 use cases:

  • Generating a token for for creating, deleting or reading users that have been created using your oauth client credentials The scopes that can be requested are user:create, user:read and user:delete
  • Generating a token to access a specific user’s data (e.g. accounts, transactions). The sub query parameter is required

Example of a client_credentials grant for creating a user

curl -X POST 'https://identity.moneyhub.co.uk/oidc/token' \
  -H 'Authorization: Basic Base64_encode(<client_id>:<client_secret>)'\
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials&scope=user%3Acreate'
// @mft/moneyhub-api-client
const {access_token} = await moneyhub.getClientCredentialTokens({
  scope: "user:create",
})

Example of a client_credentials grant for data access

curl -X POST 'https://identity.moneyhub.co.uk/oidc/token' \
  -H 'Authorization: Basic Base64_encode(<client_id>:<client_secret>)'\
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials&scope=accounts%3Aread&sub=example-user-id'
// @mft/moneyhub-api-client
const {access_token} = await moneyhub.getClientCredentialTokens({
  scope: "accounts:read transactions:read:all",
  userId: "example-user-id"
})

Authorization Code

The authorization_code grant type is implemented according to the specs in RFC6749 and OIDC, with the addition that you can send the user id as a sub parameter.

The sub parameter helps to verify that the code that is exchanged belongs to that user authorisation process.

Authorization codes last for 10 minutes, and can only be used once.

Example of an authorization_code grant request using client secret basic auth

curl -X POST 'https://identity.moneyhub.co.uk/oidc/token' \
  -H 'Authorization: Basic Base64_encode(<client_id>:<client_secret>)'\
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=authorization_code&code=example-auth-code&redirect_uri=example-redirect-uri&sub=example-user-id'

Example of an authorization_code grant request using private key jwt auth

curl -X POST 'https://identity.moneyhub.co.uk/oidc/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=authorization_code&code=aPQQLsjGonYICd4fyc6SbaCmLPE&redirect_uri=http%3A%2F%2Flocalhost%3A3001&client_id=1e1b2556-6e29-426c-9a52-8cd5b9019c72&client_assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkpXQnliRXcxc19KSWpLUFh2UHpSTlFmZExBa2o5MElKSjJoRmQ2b29xaE0ifQ.eyJpYXQiOjE2MjU2NjU4MjUsImV4cCI6MTYyNTY2NTg4NSwianRpIjoiNGw2b1h4b1NBOW9TaGZKWU1telFMcDN5RUVzc1k0RG04Vmk5cW5MQU9GMCIsImlzcyI6IjFlMWIyNTU2LTZlMjktNDI2Yy05YTUyLThjZDViOTAxOWM3MiIsInN1YiI6IjFlMWIyNTU2LTZlMjktNDI2Yy05YTUyLThjZDViOTAxOWM3MiIsImF1ZCI6Imh0dHBzOi8vaWRlbnRpdHkubW9uZXlodWIuY28udWsvb2lkYy90b2tlbiJ9.VkdC0H3demnqTWG1eDlKD--ZuovBL8N2jpNU9xCxPbg9737RWb94pAanebcc4LTy5Ci_J8vEl81dZZkEAF_P9v2lL5QvJ4OHlupv6JEzJoLjohCgnJaYFlvRd6iXHoK-gnOpMQgu4OB7jXtXRgDHopee2O86M0q-2gW_VnrKLFpA4-_p0DolgQjKB80ojr52MRN_D-sZVPBE2qAwa8ueOknYW9eTq4E2AHULfUwSzTC7_xgOqd76DWtCpwIuWvkiVylTFQ-GZzo3sI4cisnnnaNBfkTuRROyHAAoRetZRWhya8b41MoXrGpbHKwAlW3j2jCQQewhd2nCqeQ2_KqM-A&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&sub=example-user-id'

Example of an authorization_code grant request using the moneyhub api client

// @mft/moneyhub-api-client
const paramsFromCallback = {
  code: "example-auth-code",
  state: "example-state",
}
const localParams = {
  sub: "example-user-id",
  nonce: "example-nonce",
  state: "example-state",
}
const result = await moneyhub.exchangeCodeForTokens({paramsFromCallback, localParams})

Token set returned when exchanging a valid authorization code:

{
  access_token: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlRxVk1laV9XdUtqZW5HWlJUbnJpeUxXRnZuS2tzTjNvLWFuWXBqS0JEbVUifQ.eyJqdGkiOiJLYkFwQVo1UDk5R2RfckdTQXl6bDMiLCJzdWIiOiI2MDUzN2IwNzRjNGI2ZmRmMTIwMjQ2NTgiLCJpc3MiOiJodHRwczovL2lkZW50aXR5Lm1vbmV5aHViLmNvLnVrL29pZGMiLCJpYXQiOjE2MTYwODM3NDMsImV4cCI6MTYxNjA5MDk0Mywic2NvcGUiOiJpZDoxZmZlNzA0ZDM5NjI5YTkyOWM4ZTI5Mzg4MGZiNDQ5YSBvcGVuaWQiLCJjbGFpbXMiOnsiaWRfdG9rZW4iOnsic3ViIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiI2MDUzN2IwNzRjNGI2ZmRmMTIwMjQ2NTgifSwibWg6Y29uX2lkIjp7ImVzc2VudGlhbCI6dHJ1ZX0sImF1dGhfdGltZSI6eyJlc3NlbnRpYWwiOnRydWV9fSwicmVqZWN0ZWQiOltdfSwiYXVkIjoiMWUxYjI1NTYtNmUyOS00MjZjLTlhNTItOGNkNWI5MDE5YzcyIn0.eP90PFkfrh9syOpit_8SPutsjEN_KaZN23bR5VL43_tRjxKL5Rxc2M1HVMfUY2WraEEjC9fqwGpW3L3otPpzn1iZWy3SjS0iUcz1VCbucEOWvwjuAHUc0hQSqoDx97oJTnWiFZ_mGmk65xK_W4botUjaxlCu7iUUJsREB5C9vruo370Q2-m9fQZ4HthhsDxKAbjAy9v5ln6E4NUufkC7XPu3Yg1Nx8sTvI0a79XJ622t2Chy0z3QncoJIbBHawc6jbD-GfsrUMP0PEdB9RTlbSkn2mt1I8KpUomyWJ4E05ys3CuIoiWP2b6MMHRFgeAJinIN06uQZ0eQblySsC-urw',
  expires_at: 1616090946,
  id_token: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlRxVk1laV9XdUtqZW5HWlJUbnJpeUxXRnZuS2tzTjNvLWFuWXBqS0JEbVUifQ.eyJzdWIiOiI2MDUzN2IwNzRjNGI2ZmRmMTIwMjQ2NTgiLCJtaDpjb25faWQiOiIxZmZlNzA0ZDM5NjI5YTkyOWM4ZTI5Mzg4MGZiNDQ5YTowNjczN2ZmZS1jZGZmLTQwNDctYTFkMy1iNTRlYmM4YzM2OGIiLCJhdXRoX3RpbWUiOjE2MTYwODM3MzAsIm5vbmNlIjoiYmFyIiwiYXRfaGFzaCI6IjI1ODg2UXNaTDZIY2VfZ3Y5YU9XUWciLCJzaWQiOiI3N2I1OGZjMS0wZTMwLTQxMjMtYWVmZi04YzcwZTczMDJmMzIiLCJhdWQiOiIxZTFiMjU1Ni02ZTI5LTQyNmMtOWE1Mi04Y2Q1YjkwMTljNzIiLCJleHAiOjE2MTYwODczNDYsImlhdCI6MTYxNjA4Mzc0NiwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS5tb25leWh1Yi5jby51ay9vaWRjIn0.Upy5ThfFepYYldLL_RVBFMKjBoIWnUfZM6INF2v7572gH3B4fWBuTfQGvWHuf1NJ9szmiNN1_g09b_XI9lSCPzIXnrS-jeEgiMuCbiZISxxbRgS0Swg2XFay2LlgcWTijRBcq2r9o0KpacoXtxU2OSjJY6Q2ahF8H6HRwQHL0-zZAIj_XL8iMvgmXDwIjifSXj9wCxljS1R_3rRr1IYLIxvoSKK9NhJet4PHv-ICbApR5tFjN7ulVjR6V1rEkQDAvj9WmZFL3L2BMKEV9ZvhU773bHATNJ-ehebWr5qXzDxRsGgB4MeWc-Fy0cz4DqiiHwhNTr3sBdD1Pge0gIDv-w',
  scope: 'id:1ffe704d39629a929c8e293880fb449a openid',
  token_type: 'Bearer'
}

The id token can be decoded to get more information about the user and the connection or payment performed:

{
  sub: '60537b074c4b6fdf12024658', // userId
  'mh:con_id': '1ffe704d39629a929c8e293880fb449a:06737ffe-cdff-4047-a1d3-b54ebc8c368b',
  auth_time: 1616083730,
  nonce: 'bar',
  at_hash: '25886QsZL6Hce_gv9aOWQg',
  sid: '77b58fc1-0e30-4123-aeff-8c70e7302f32',
  aud: '1e1b2556-6e29-426c-9a52-8cd5b9019c72',
  exp: 1616087346,
  iat: 1616083746,
  iss: 'https://identity.moneyhub.co.uk/oidc'
}

Refresh Token

The refresh_token grant type is implemented exactly as according to the specs in RFC6749 and OIDC.

Request Object

We support the use of request objects in the authorization end point. The main use cases for this is to generate an authorization URL for making a connection, whether it's a connection to get account data (including beneficiaries and standing orders), creating a payment or creating a standing order and for getting tokens.

The request object allows you to sign the request parameters and prevents tampering. This can prevent a certain class of attacks against OAuth 2.0.

A request object is a JWT that has the auth request parameters encoded within it, and signed according to the configuration given in your API client. When not in production mode, the JWT won't be signed (as your client will be configured with request object signing algorithm of none). When in production, you will set an algorithm here and either provide a public JWKS or URL that points to a set of public JWKS. The private JWKS will be used to sign this request object JWT so that we can then verify the auth request has come from you, using your public key.

The body of the request object can have normal OpenID authorization URL query parameters like:

  • scope
  • client_id
  • state
  • nonce
  • redirect_url
  • etc

As well as the above, a claims object is provided to include other information about the connection you wish to make. More information on that can be found here

The request object in the auth end point is put in a query parameter of request

If your client has a token endpoint authentication method of private_key_jwt you will also need a request object when getting a token. The request object will have the same properties as above (there will be no need for the claims though). The request object is put in the token end point body in the property client_assertion. The client_assertion_type will need to be set to urn:ietf:params:oauth:client-assertion-type:jwt-bearer.

Examples

Here are a couple of examples of what request objects can look like for our auth server.

Authorization Endpoint

eyJhbGciOiJSUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QiLCJraWQiOiJkVEJicGt0RFE5ZmtSVG5TVjhrWW8tOV9maHFQUDZzQ0ttNlkxdHpUYktrIn0.eyJjbGllbnRfaWQiOiI5MWQ2Mjg2Ny1hOGE4LTQxNDYtOGE1Yi1kNGU2MTIyN2RiMjEiLCJzY29wZSI6ImlkOjFmZmU3MDRkMzk2MjlhOTI5YzhlMjkzODgwZmI0NDlhIG9wZW5pZCIsInN0YXRlIjoiZm9vIiwibm9uY2UiOiJiYXIiLCJyZWRpcmVjdF91cmkiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJyZXNwb25zZV90eXBlIjoiY29kZSIsInByb21wdCI6ImNvbnNlbnQiLCJjbGFpbXMiOnsiaWRfdG9rZW4iOnsic3ViIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiI2MGI4OWJkMDc3MmQyOGIyNzMxZDBjZjIifSwibWg6Y29uX2lkIjp7ImVzc2VudGlhbCI6dHJ1ZX0sIm1oOmNvbnNlbnQiOnsiZXNzZW50aWFsIjp0cnVlLCJ2YWx1ZSI6eyJwZXJtaXNzaW9ucyI6WyJSZWFkU3RhbmRpbmdPcmRlcnNCYXNpYyJdfX19fSwibWF4X2FnZSI6ODY0MDAsImlzcyI6IjkxZDYyODY3LWE4YTgtNDE0Ni04YTViLWQ0ZTYxMjI3ZGIyMSIsImF1ZCI6Imh0dHA6Ly9pZGVudGl0eS5kZXYuMTI3LjAuMC4xLm5pcC5pby9vaWRjIiwianRpIjoibmxUOWJoY0wxQkhNUmhhT1BnTjNESHRxZXJ4emRCNmsxX2F5ajFHQ1gzRSIsImlhdCI6MTYyMjgxMjU4OCwiZXhwIjoxNjIyODEyODg4fQ.WoweEp4CklmmwOoj_CM1m_Zf4bJRdgnM_zYX7COOzcXCz5oDSp-xcozWaFwDYdxM6fFuADxqvU_LlzuYrmF-Hj4TxolWpgoDaItfs9XytmuWdQkWHoqhPtFbNld2BrtThI4VefQx7nuAs0rKXMCXCcpp2nW8Zkqi4WgP4miCq3lqgkGX3rY_0eDL3otvN2PizPu3kP3MBfurU81K9K1PGb6SgatBOcAL6Et8QVcvVAnJ_88W1gLKBwO4L_ZXsqiw7JYV_yrSbJsr_qsQIfDcn6xpYaUaUi10oYc02FLOmRpf8A_QCoGyL2dZvr6T9u4sTcP4sSlpqz3iq58o8yGLTgz

{
  "client_id": "91d62867-a8a8-4146-8a5b-d4e61227db21",
  "scope": "id:1ffe704d39629a929c8e293880fb449a openid",
  "state": "foo",
  "nonce": "bar",
  "redirect_uri": "http://localhost:3001",
  "response_type": "code",
  "prompt": "consent",
  "claims": {
    "id_token": {
      "sub": {
        "essential": true,
        "value": "60b89bd0772d28b2731d0cf2"
      },
      "mh:con_id": {
        "essential": true
      },
      "mh:consent": {
        "essential": true,
        "value": {
          "permissions": [
            "ReadStandingOrdersBasic"
          ]
        }
      }
    }
  },
  "max_age": 86400,
  "iss": "91d62867-a8a8-4146-8a5b-d4e61227db21",
  "aud": "https://identity.moneyhub.co.uk/oidc",
  "jti": "nlT9bhcL1BHMRhaOPgN3DHtqerxzdB6k1_ayj1GCX3E",
  "iat": 1622812588,
  "exp": 1622812888
}

Token Endpoint

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlRxVk1laV9XdUtqZW5HWlJUbnJpeUxXRnZuS2tzTjNvLWFuWXBqS0JEbVUifQ.eyJqdGkiOiJmRnVjb0JpZTc2MGhJcXA3Vn5CeGQiLCJpc3MiOiJodHRwczovL2lkZW50aXR5Lm1vbmV5aHViLmNvLnVrL29pZGMiLCJpYXQiOjE2MjI4MTIzNzUsImV4cCI6MTYyMjgxMjk3NSwic2NvcGUiOiJ1c2VyOnJlYWQiLCJhdWQiOiI5NzlhYmRhYi1mZDY0LTQyZTYtYWJkMi1lZTc0NTQyZjM0OTMifQ.E0FJk9gJaPXJzsQj2mWT5W4WTX4KNQtI0rnpuBHonexwbLV1jwz62rLkVDeevfVisdNst4K_bl5XsU8W4akuj4BTai5IPZq_CF5Zi6UGU5psD7cPYvqBNTvg41A2hkNwFPU8GJNd3fwhmuqkkzwH_JYAZlWTK_NYo1kp98lxYCmDdG2dWzHUXkq2FDi7k-mNHs2pjcOOEPfknvsOHyDpsC0R8nJaJjDPFSUIkwoUxxrt1fuQ26QbFKUNzUZponq9HsJeFsbFX5YnLnRM3Nqfo6lLT6vZNsnYEv5tbXGD_EUhk01cuOQZuBvspbm3F7tIVrgmRgiMgbVsyGbEx_wFSg

{
  "jti": "fFucoBie760hIqp7V~Bxd",
  "iss": "https://identity.moneyhub.co.uk/oidc",
  "iat": 1622812375,
  "exp": 1622812975,
  "scope": "user:read",
  "aud": "979abdab-fd64-42e6-abd2-ee74542f3493"
}

Notes

  • You will need to provide a unique token identifier and set it to the jti property for all request objects.
  • An issued at and expiry time will be required in request objects.
  • We would recommend using our API Client Library to generate these auth URLs and token requests if you are developing in JavaScript, or to find an OpenID Connect library for your chosen language that supports request objects.

More information about request objects is available here

JWKS Endpoints & Asymmetric Signatures

We use jwks endpoints to support the rotation of signing keys

As part of registering your client software with us, we will ask you for your own jwks endpoint.

If you don’t yet have one, we strongly encourage you to implement one. JWKS endpoints provide a neat method to achieve key rotation without any interruption to service or any need for bilateral communication.

They enable you to manage your keys in which ever manner you see fit and removes the need for the client_secret.

For more information about the benefits of this approach or advice on implementing, please contact us.

Code Example

Below is a Python code example of making a client_credentials grant for a client with a private key authentication method.

import jwt, jwcrypto.jwk as jwk, datetime, requests
from datetime import datetime, timedelta, timezone
import time
import random
import string

letters = string.ascii_lowercase

client_id = "client_id here"
private_key = {
}
key = jwk.JWK(**private_key)

def generate_jti():
  return "".join(random.choice(letters) for i in range(32))

def generate_request_object(client_id, identity_server):
  iat = datetime.now(timezone.utc)
  exp = datetime.now(timezone.utc) + timedelta(hours=1)

  payload = { 
    "iss": client_id,
    "sub": client_id,
    "aud": "{}/oidc/token".format(identity_server),
    "iat": time.mktime(iat.timetuple()),
    "exp": time.mktime(exp.timetuple()),
    "jti": generate_jti()
  }

  return jwt.encode(
    payload,
    key.export_to_pem(private_key=True, password=None), 
    algorithm="RS256",
  )

def get_client_credentials_token(client_id, private_key, scope, identity_server = "https://identity.moneyhub.co.uk"):
  request_object = generate_request_object(client_id=client_id, identity_server=identity_server)
  params = {
    "scope": scope,
    "client_id": client_id, 
    "grant_type": "client_credentials",
    "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
    "client_assertion": request_object,
  }

  headers = {
    "Content-Type": "application/x-www-form-urlencoded",
  }

  return requests.post("{}/oidc/token".format(identity_server), data=params, headers=headers)

r = get_client_credentials_token(client_id=client_id, private_key=private_key, scope="payee:create")

print(r.text)