Stitch SSO
4.4. User Tokens
This section will walk you through being able to sign in with Stitch SSO.
While we strongly recommend going through this section and familiarising yourself with Stitch SSO, there are many libraries which implement OpenID Connect Standards mentioned in section 5.5 and will perform much of the below mentioned functionality.
Before continuing, it may be useful to consider the following diagram. The diagram serves as a map of the terrain we need to navigate; namely the process required to acquire a token to interact with the API outside the IDE:
Getting a user access token that can query the API is a 2-step process:
- Obtaining an authorization code
- Using the authorization code to obtain user access and refresh tokens
Obtaining an Authorization Code
The first step in the process entails navigating the browser (or app) to the https://secure.stitch.money/connect/authorize endpoint. If the query string parameters passed to the endpoint are correctly formed, the user is redirected to the login and consent UI.
The table below lists the required parameters. Note that all values should be URL encoded. To complete the rest of the tutorial, you'll need at least the openid
, accounts
, balances
, and offline_access
scopes.
Authorization Request Query Parameters | |
---|---|
Parameter | Description |
client_id | This is the unique ID of the client you generated |
scope | A space separated list of requested scopes. The If you want to use a refresh token, request the Stitch API specific scopes are:
To determine which scopes are required for specific queries, please consult the Stitch API reference |
response_type | Should always have a value of "code" . Instructs Stitch SSO to return an authorization code |
redirect_uri | One of a whitelisted set of URLs. After login, the user is redirected back to this URL. The redirect_uri is whitelisted to prevent open redirect attacks |
nonce | A nonce is required to mitigate replay attacks. The value of the nonce should be a cryptographically secure random string, and is later included in the id_token , found in the token endpoint response. |
state | The state parameter is required to prevent CSRF (Cross Site Request Forgery). Like the nonce, this should be a cryptographically secure random value. The value of the state parameter should be stored in the application (e.g. in local storage, or as a cookie). When the authorization request returns, the state is included, and should be validated against the stored value to properly protect against CSRF |
code_challenge | A base64URL encoding of the SHA256 hashed code_verifier created below |
code_challenge_method | The hash algorithm used for the code_challenge. In this case the value will be "S256" (case-sensitive) |
Generating a Code Verifier
In order for the server to correlate the authorization and user access code requests, a unique code_verifier
needs to be generated for each request. The hashed value of this code (the code_challenge
) is sent with the authorization code request, and the unhashed value, the code_verifier
, is sent with the user access token request.
This code_verifier
, code_challenge
pair is needed to confirm to the SSO server that the token request is coming from the same party as the authorization request, preventing several classes of vulnerabilities.
The code_verifier
is a cryptographically random string, between 43 and 128 characters long, using the characters A-Z
, a-z
, 0-9
, and the punctuation characters -._~
(hyphen, period, underscore, and tilde).
The following JavaScript code can be used to generate the verifier challenge pair in modern browsers:
1function base64UrlEncode(byteArray) {2 const charCodes = String.fromCharCode(...byteArray);3 return window.btoa(charCodes)4 .replace(/\+/g, '-')5 .replace(/\//g, '_')6 .replace(/=/g, '');7}89async function sha256(verifier) {10 const msgBuffer = new TextEncoder('utf-8').encode(verifier);1112 // hash the message13 const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);1415 return new Uint8Array(hashBuffer);16}1718async function generateVerifierChallengePair() {19 const randomBytes = crypto.getRandomValues(new Uint8Array(32));2021 const verifier = base64UrlEncode(randomBytes);22 console.log('Verifier:', verifier);2324 const challenge = await sha256(verifier).then(base64UrlEncode);25 console.log('Challenge:', challenge)2627 return [verifier, challenge];28}2930generateVerifierChallengePair();
To verify your code verifier and code challenge pair, paste them into the inputs below.
Reminder: A unique pair needs to be generated for each request.
Generating the State and Nonce
The state
and nonce
fields are required for making an authorization request. The state
is required to prevent cross site request forgery (CSRF), while the nonce
is required to prevent replay attacks. In the Stitch IDE, we use the state
parameter as a key in localStorage to store the code_verifier
and nonce
. The state
is returned when authorization request completes, so is convenient for that purpose, while also mitigating CSRF.
The following JavaScript code generates valid state
and nonce
values:
1function base64UrlEncode(byteArray) {2 const charCodes = String.fromCharCode(...byteArray);3 return window.btoa(charCodes)4 .replace(/\+/g, '-')5 .replace(/\//g, '_')6 .replace(/=/g, '');7}89function generateRandomStateOrNonce() {10 const randomBytes = crypto.getRandomValues(new Uint8Array(32));11 return base64UrlEncode(randomBytes);12}1314const state = generateRandomStateOrNonce();15console.log('State:', state);1617const nonce = generateRandomStateOrNonce();18console.log('Nonce:', nonce);
Building the URL
Once you've created a verifier challenge pair, the state, and the nonce, you can finally create an authorization request url. The following JavaScript code helps you build your URL:
1function buildAuthorizationUrl(clientId, challenge, redirectUri, state, nonce, scopes) {2 const search = {3 client_id: clientId,4 code_challenge: challenge,5 code_challenge_method: 'S256',6 redirect_uri: redirectUri,7 scope: scopes.join(' '),8 response_type: 'code',9 nonce: nonce,10 state: state11 };12 const searchString = Object.entries(search).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');13 return `https://secure.stitch.money/connect/authorize?${searchString}`;14}
Example URL
Here is an example of a plausible connect URL. Note that the client_id
and redirect_uri
values would need to be replaced with your own for this example to work.
1https://secure.stitch.money/connect/authorize?client_id=test-18fbd892-3b73-43c3-a854-c6f78c681349&scope=openid%20offline_access%20transactions%20accounts&response_type=code&redirect_uri=https%3A%2F%2Flocalhost%3A9000%2Freturn&state=2669382d-d6df-4331-a58b-d5743961578b&nonce=5138fa17-ed63-41e5-98b6-047f07efd940&code_challenge=FVF6VCwYJ_XljGdLXVjxs6g-_QRh4_CDutLOf_oIzPw&code_challenge_method=S256
Some browsers may mangle the URL if you paste it into the address bar. If that is the case, it may be helpful to paste in this URL which is the same as the above, just without the URL encoding:
1https://secure.stitch.money/connect/authorize?client_id=test-18fbd892-3b73-43c3-a854-c6f78c681349&scope=openid offline_access transactions accounts&response_type=code&redirect_uri=https://localhost:9000/return&state=2669382d-d6df-4331-a58b-d5743961578b&nonce=5138fa17-ed63-41e5-98b6-047f07efd940&code_challenge=FVF6VCwYJ_XljGdLXVjxs6g-_QRh4_CDutLOf_oIzPw&code_challenge_method=S256
Decoding the Response
If the connect URL is correctly formed, and after the user has signed in and granted access, they'll be redirected back to the redirect_uri
.
The value of the query string (the part of a URL delimited by a ?
), if all went well, will contain the following URL encoded parameters:
Authorization Response Fragment Parameters | |
---|---|
Parameter | Description |
code | The authorization code needed to obtain a user access token |
scope | A space delimited list of scopes that were granted by the user |
state | The string that was passed in as the state parameter to the authorization request |
session_state | An opaque string that represents the End-User's login state. It can be used in advanced scenarios to verify whether a user session is still active (see https://openid.net/specs/openid-connect-session-1_0.html) |
You'll need to grab the code
from the query string to continue onto the next step, along with the redirect_uri
supplied to the connect call. Users may choose to cancel while signing in, in which case they will be redirected without an authorization code in the query string. This case should be handled before attempting to retrieve a user access token.
⚠️ Note
In the event that the authorization request fails (eg. a user chooses to quit the session), then the response will include only the state
parameter above.
Using an Authorization Code to retrieve User Access and Refresh Tokens
The next step entails retrieving an API user access token from the https://secure.stitch.money/connect/token endpoint.
In order to do so, a client_assertion
first needs to be generated. Follow the client assertion guide to learn how to generate the client assertion.
To retrieve the token, make a POST request to the endpoint, with a content type of application/x-www-form-urlencoded
and the following fields in the body:
Token Request Body Parameters | |
---|---|
Parameter | Description |
grant_type | For the purposes of retrieving the token, should always be "authorization_code" |
client_id | This is the unique ID of the client you generated. The same as the client_id used in the previous step |
code | The code retrieved from the previous step |
redirect_uri | The redirect_uri used in the previous step |
code_verifier | The unhashed code_verifier generated in the previous step |
client_assertion_type | Should always have the value urn:ietf:params:oauth:client-assertion-type:jwt-bearer |
client_assertion | The value of the generated private_key_jwt |
Note that an authorization code can only be used once, thereafter becoming invalid. This is to mitigate opportunities for replay attacks.
Retrieving the Token Using cURL
This example bash script uses cURL to retrieve the user access and refresh token.
You'll need to replace the clientId
, authorizationCode
, codeVerifier
and redirectUri
with the appropriate values. This request if correctly formed, will return a JSON payload with the token.
1clientId='test-18fbd892-3b73-43c3-a854-c6f78c681349'2authorizationCode='DH7-TaofOSCFlsQwZAeEfmap1eXPeH7nmeOMtDJhdOw'3redirectUri='https%3A%2F%2Flocalhost%3A9000%2Freturn'4codeVerifier='chy0EHtabciY0kjRzCDr113O2slAa-xY_zvdApkXxvw'5clientAssertion='<your client assertion>'6clientAssertionType='urn:ietf:params:oauth:client-assertion-type:jwt-bearer'78curl -X POST \9 https://secure.stitch.money/connect/token \10 -H 'Content-Type: application/x-www-form-urlencoded' \11 -d "grant_type=authorization_code&client_id=$clientId&code=$authorizationCode&redirect_uri=$redirectUri&code_verifier=$codeVerifier&client_assertion=$clientAssertion&client_assertion_type=$clientAssertionType"
Retrieving the Token Using JavaScript and the Fetch API
1async function retrieveTokenUsingAuthorizationCode(clientId, redirectUri, verifier, code, clientAssertion) {2 const body = {3 grant_type: 'authorization_code',4 client_id: clientId,5 code: code,6 redirect_uri: redirectUri,7 code_verifier: verifier,8 client_assertion: clientAssertion,9 client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'10 }11 const bodyString = Object.entries(body).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');1213 const response = await fetch('https://secure.stitch.money/connect/token', {14 method: 'post',15 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },16 body: bodyString,17 });1819 const responseBody = await response.json();20 console.log('Tokens: ', responseBody);21 return responseBody;22}
Retrieving the Token Using Postman
Download the following Postman collection, and import it into Postman:
Getting Started.postman_collection.json
The first request in the collection is "Authorization Code". Replace the entries in the Body tab with the appropriate values, and click send. The request if correctly formed will return a JSON payload with the token.
Response Body
A typical response body returned from the token endpoint will look like the following:
1{2 "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Im9TbWt2RmhqVWNia0I4MjRrek5fWkEiLCJ0eXAiOiJKV1QifQ.eyJuYmYiOjE1NzYxMzg4OTgsImV4cCI6MTU3NjEzOTE5OCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAxIiwiYXVkIjoiZjVjYzk0ZWUtZTk1MS00MGQzLWEzMWUtNzk0NjU5ODA2MjMwIiwibm9uY2UiOiJ4eXoiLCJpYXQiOjE1NzYxMzg4OTgsImF0X2hhc2giOiJqcFVVcGh5VWtaQ3diUXNZaG5zNmF3Iiwic19oYXNoIjoidW5nV3Y0OEJ6LXBCUVVEZVhhNGlJdyIsInNpZCI6InM0LWd0WXFNQUFQWkhfcy1tdjBsa3ciLCJzdWIiOiJ1c2VyL2Y1Y2M5NGVlLWU5NTEtNDBkMy1hMzFlLTc5NDY1OTgwNjIzMC9mbmIvOTdjY2M1ZmQtY2I5NS00NjViLWE0ZGMtNzYxMzk2MzNiMDdiIiwiYXV0aF90aW1lIjoxNTc2MTM3MjkxLCJpZHAiOiJsb2NhbCIsImFtciI6WyJwd2QiXX0.nVKdmdOZbNaN1lutLmvaT_nIf1kYKphXNzFHioapDigT3pNwYwmDVGQ54V40kvwJBF8PiQBzfp2hdV1S6ltJqWmnuEtLteg240FimYMdrX1sD3nQKzoMKVkbComY5lnB79QvFc5-dWRCauvZW9LY0mhnXdzRFXhb-Jv21XwzMhE_y21vPAVdPrk0jB7jyk15OZ82RwwlZYhdcBUP7Se4BMvvr4wJJEBWgxAIciFDa6gLqDyL-eInYePjhglV6KrrIHHM9C3PEd1kJJHD6uDkOf3858kS9Nr1Mnj2oNJRvpahWq8VhItsw_5JItfwaTYQoHN25Zk4A0a5Nox6weD-RA",3 "access_token": "nXPh9P16drRQLnAmwy9Sf072U81KVNNa6iqduWX6kK4",4 "expires_in": 3599,5 "token_type": "Bearer",6 "refresh_token": "Vnzqys6mDyRAOz_ZOb6LOv2keOmyvOLlHEd1s6Hc2Xg",7 "scope": "openid transactions accounts offline_access"8}
Note that if the user did not grant permission to have the offline_access
scope, the refresh_token
will be omitted from the response.
Authorization Code Response Body Parameters | |
---|---|
Parameter | Description |
id_token | id_token contains user profile information and is JWT encoded |
access_token | The token needed to query the Stitch API |
expires_in | The number of seconds until the token expires |
refresh_token | The refresh_token which needs to be stored for use in later steps |
scope | The scopes that were granted by the user |
Making API calls with the User Access Token
This section serves to show you how to make an authenticated GraphQL query against the Stitch API using cURL, JavaScript and Postman. Integrating Stitch API goes into more detail about how the GraphQL protocol works over HTTP.
Making an Authenticated Query Using cURL
Replace the accessToken
variable in the script below with the one issued in the previous step:
1accessToken='M6kGvDwSCBUx2oYy-CnphEdO7Ad9TnHL1fWaNaSaDSo'23curl -X POST \4 https://api.stitch.money/graphql \5 -H 'Content-Type: application/json' \6 -H "Authorization: Bearer $accessToken" \7 -d '{"query":"query ListBankAccounts {\n user {\n __typename\n ... on User {\n id\n bankAccounts {\n name\n }\n }\n }\n}\n","variables":null,"operationName":"ListBankAccounts"}'
Making an Authenticated Query using JavaScript and the Fetch API
The function below can be used to make queries to the StitchApi:
1function queryStitchApi(accessToken) {23 var myHeaders = new Headers();4 myHeaders.append("Content-Type", "application/json");5 myHeaders.append("Authorization", `Bearer ${accessToken}`);67 var graphql = JSON.stringify({8 query: "query ListBankAccounts { user { bankAccounts { user { bankAccounts { name }}}}}",9 variables: {}10 })1112 var requestOptions = {13 credentials: 'include',14 method: 'POST',15 headers: myHeaders,16 body: graphql,17 mode: 'cors'18 };1920 return fetch('https://api.stitch.money/graphql', requestOptions);21}
Making an Authenticated query using Postman
Download the following Postman collection, and import it into Postman:
Getting Started.postman_collection.json
Replace the value of the Authorization
header in the headers section of Postman with Bearer {{YOUR_TOKEN}}
. This should allow you to make queries against the API.
Using the Refresh Token to Create a New Session
Eventually, the user access token you've been using will expire, and API calls will start returning a status code of 401
. If that is the case, it means it's time to make use of the refresh token to generate a new token (and a new refresh token as well). This process also uses the https://secure.stitch.money/connect/token endpoint. The call should have the following parameters:
Refresh Token Request Body Parameters | |
---|---|
Parameter | Description |
grant_type | For the purposes of using the refresh token, should always be the value refresh_token |
client_id | This is the unique ID of the client you generated. This is the same as the client_id used in the previous step |
refresh_token | The refresh_token that was obtained from the authorization code response |
client_assertion_type | Should always have the value urn:ietf:params:oauth:client-assertion-type:jwt-bearer |
client_assertion | The value of the generated private_key_jwt |
⚠Refresh token expiry and lifetime
Refresh tokens have a sliding expiry of 2 months, and an absolute refresh lifetime of 2 years. Exact values for these durations can be found in your client credentials JSON.
It is suggested that if you are storing refresh tokens for recurring offline access to a user's account, you should consider having a job run periodically to keep any tokens that are nearing expiry, fresh.
Note that a refresh token can only be used once, the result of the token call will include a new access and refresh token.
Requesting a new token also creates a new banking session. Depending on the bank, this means that it may trigger a login notification, or bring up a second-factor prompt. The latter case will not interrupt the process of retrieving a new access token, but follow-up requests to the API may require user interaction.
Refreshing the Token Using cURL
This example bash script uses cURL to retrieve the user access and refresh token.
You'll need to replace the clientId
and refreshToken
with the appropriate values.
This request if correctly formed, will return a JSON payload with the token
1clientId='test-18fbd892-3b73-43c3-a854-c6f78c681349'2refreshToken='Vnzqys6mDyRAOz_ZOb6LOv2keOmyvOLlHEd1s6Hc2Xg'3clientAssertion='<your client assertion>'4clientAssertionType='urn:ietf:params:oauth:client-assertion-type:jwt-bearer'56curl -X POST \7 https://secure.stitch.money/connect/token \8 -H 'Content-Type: application/x-www-form-urlencoded' \9 -d "grant_type=refresh_token&client_id=$clientId&refresh_token=$refreshToken&client_assertion=$clientAssertion&client_assertion_type=$clientAssertionType"
Refreshing the Token Using JavaScript and the Fetch API
1async function retrieveTokenUsingRefreshToken(clientId, refreshToken, clientAssertion) {2 const body = {3 grant_type: 'refresh_token',4 client_id: clientId,5 refresh_token: refreshToken,6 client_assertion: clientAssertion,7 client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'8 }9 const bodyString = Object.entries(body).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');1011 const response = await fetch('https://secure.stitch.money/connect/token', {12 method: 'post',13 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },14 body: bodyString,15 });1617 const responseBody = await response.json();18 console.log('Tokens: ', responseBody);19 return responseBody;20}
Refreshing the Token Using Postman
Download the following Postman collection, and import it into Postman:
Getting Started.postman_collection.json
The second request in the collection is "Refresh Token". Replace the entries in the Body tab with the appropriate values and click send. The request if correctly formed will return a JSON payload with the token.
Response Body
A typical response body returned from the refresh token endpoint will look like the following:
1{2 "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Im9TbWt2RmhqVWNia0I4MjRrek5fWkEiLCJ0eXAiOiJKV1QifQ.eyJuYmYiOjE1NzYyMjM1MjEsImV4cCI6MTU3NjIyMzgyMSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAxIiwiYXVkIjoiZjVjYzk0ZWUtZTk1MS00MGQzLWEzMWUtNzk0NjU5ODA2MjMwIiwiaWF0IjoxNTc2MjIzNTIxLCJhdF9oYXNoIjoiZmNaeVN5MnBSYlBKMnUwSW1OWUNlQSIsInN1YiI6InVzZXIvZjVjYzk0ZWUtZTk1MS00MGQzLWEzMWUtNzk0NjU5ODA2MjMwL2ZuYi9kY2QxMzczNy0xMTc4LTRhMTUtOGQwYy00MGE0YzBiMmU1ODkiLCJhdXRoX3RpbWUiOjE1NzYyMjM0MTAsImlkcCI6ImxvY2FsIiwiYW1yIjpbInB3ZCJdfQ.ISRH-W9SgDJmTAcAFcrRggDb4Ym-0xmZ-Lbv1gfceUr00wjV8eiZcf_EE1Ca9t7fXgeFMsWTclc-ZxX5szWQAvLaqGdFo-3IlKuPgmftTmfTAb1y7_RWNIjuTjDtJvWzLnf1WGO62Ki_uz2kB3VicneDEzSx5YJRGJz6tZ5LBvrCAj_WQ4mpodNEawuC1komJMhROVtLdM7tAAE7BPhF3Ks0v-SiAzh8QtxBAs8l-13dcmgeXrgCiND5i520QtuOhdVV7DNQ1BvP8SGxMhmuA4V5s3V2BDTIUkR8_IsMq6OE-BxjXfflM1QGlV7_tRs52w5CLxfuFNX_sVIHNIWQzw",3 "access_token": "ey-K-qPC5Cs0ERd0eDuLJI636rLEnd3Kwi5L1NrVNJY",4 "expires_in": 3599,5 "token_type": "Bearer",6 "refresh_token": "Wce1_ujqCD8B-BHAzrV-1S_3WFsHSxXKWUHGtfJvZvc",7 "scope": "openid accounts transactions offline_access"8}
Refresh Token Response Body Parameters | |
---|---|
Parameter | Description |
id_token | Same as the one returned from the authorize call. id_token contains user profile information and is JWT encoded |
access_token | The token needed to query the Stitch API |
expires_in | The number of seconds until the token expires |
refresh_token | The next refresh_token which needs to be stored for later use |
scope | The scopes that were granted by the user |
Token Expiry
User Access tokens are configured to have a 1-hour lifetime by default.
The token expiry is indicated by the expires_in
field (displayed in seconds) which is returned when successfully retrieving a token.
You can refresh your token before the indicated time to ensure you always have a fresh token
Queries with an expired token will yield the following response:
1{2 "errors": [3 {4 "message": "UNAUTHENTICATED: Token is expired or malformed"5 }6 ],7 "extensions": {8 "code": "UNAUTHENTICATED"9 }10}