What are User Tokens?
Most of the functionality of the Stitch API is protected by user tokens. A user token represent a client's authorization to access certain features of the user's bank account.
While we strongly recommend going through this section and familiarising yourself with Stitch SafeLink, there are many libraries which implement OpenID Connect Standards mentioned in the FrontEnd Integration section and will perform much of the functionality required.
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:
Acquiring a user token that can query the API involves a 2-step process:
- Obtain an authorization code
- Use 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.
Parameter | Description |
---|---|
client_id | This is the unique ID of the client you generated. |
scope | You'll need a non-empty, space-separated list of requested scopes. The openid scope is required. If you want to use a refresh token, request the offline_access scope. Stitch API-specific scopes include: accounts, transactions, balances, accountholders. To determine which scopes are required for specific queries, please consult the Stitch API reference. |
response_type | Instructs Stitch to return an authorization code. This should always have a value of "code". |
redirect_uri | One of a whitelisted set of URLs. After login, the user should be redirected back to this URL. The redirect_uri is whitelisted to prevent open redirect attacks. This should always be protected by SSL and HSTS. |
nonce | A nonce is required to mitigate replay attacks. The value of the nonce should be a cryptographically secure, random string between 32 and 300 characters in length. The nonce 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 string between 32 and 300 characters in length. 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 secure, random string between 43 and 128 characters long. The code_verifier
consists of the characters A-Z
, a-z
, 0-9
, and the punctuation characters -._~
(hyphen, period, underscore, and tilde).
The following code snippets show how to generate the code_verifier
and code_challenge
pairs across a few common languages:
- JavaScript
- .NET
- Python 3
- Java
- PHP
async function sha256(verifier) {
const msgBuffer = new TextEncoder("utf-8").encode(verifier);
// hash the message
const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
return new Uint8Array(hashBuffer);
}
async function generateVerifierChallengePair() {
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
const verifier = base64UrlEncode(randomBytes);
console.log("Verifier:", verifier);
const challenge = await sha256(verifier).then(base64UrlEncode);
console.log("Challenge:", challenge);
return [verifier, challenge];
}
generateVerifierChallengePair();
using System;
using System.Security.Cryptography;
using System.Text;
public class Program
{
public static void Main()
{
var rng = RandomNumberGenerator.Create();
var bytes = new byte[32];
rng.GetBytes(bytes);
var codeVerifier = Convert.ToBase64String(bytes)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
var codeChallenge = string.Empty;
using (var sha256 = SHA256.Create())
{
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
codeChallenge = Convert.ToBase64String(challengeBytes)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
Console.WriteLine($"codeVerifier value {codeVerifier}");
Console.WriteLine($"codeChallenge value {codeChallenge}");
}
}
import secrets
import hashlib
import base64
# this should be between 43 and 128
verifier_length = 43
code_verifier = secrets.token_urlsafe(96)[:verifier_length]
hashed = hashlib.sha256(code_verifier.encode('ascii')).digest()
encoded = base64.urlsafe_b64encode(hashed)
code_challenge = encoded.decode('ascii')[:-1]
print("code_verifier value", code_verifier)
print("code_challenge value", code_challenge)
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
class Main {
static String generateCodeVerifier() throws UnsupportedEncodingException {
SecureRandom secureRandom = new SecureRandom();
byte[] codeVerifier = new byte[32];
secureRandom.nextBytes(codeVerifier);
return Base64.getUrlEncoder().withoutPadding().encodeToString(codeVerifier);
}
static String generateCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException {
byte[] bytes = codeVerifier.getBytes(StandardCharsets.UTF_8);
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(bytes, 0, bytes.length);
byte[] digest = messageDigest.digest();
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
public static void main() {
try {
String codeVerifier = generateCodeVerifier();
System.out.println("codeVerifier value " + codeVerifier);
String codeChallenge = generateCodeChallange(codeVerifier);
System.out.println("codeChallenge value " + codeChallenge);
} catch (NoSuchAlgorithmException ex) {
System.out.println("Error generating codeVerifier and codeChallenge " + ex);
}
}
}
function base64url_encode($plainText)
{
$base64 = base64_encode($plainText);
$base64 = trim($base64, '=');
return strtr($base64, '+/', '-_');
}
$random = bin2hex(openssl_random_pseudo_bytes(32));
$code_verifier = base64url_encode(pack('H*', $random));
$code_challenge = base64url_encode(pack('H*', hash('sha256', $code_verifier)));
echo "code_verifier value " . $code_verifier;
echo "code_challenge value " . $code_challenge;
Code Verifier and Challenge Validation
To verify your code verifier and code challenge pair, paste them into the inputs below.
A unique pair needs to be generated for each request.
Generating the State and Nonce
The state
and nonce
fields are required to make 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 the authorization
request is complete and helps mitigate CSRF.
The following JavaScript code generates valid state
and nonce
values:
function base64UrlEncode(byteArray) {
const charCodes = String.fromCharCode(...byteArray);
return window
.btoa(charCodes)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
function generateRandomStateOrNonce() {
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
return base64UrlEncode(randomBytes);
}
const state = generateRandomStateOrNonce();
console.log("State:", state);
const nonce = generateRandomStateOrNonce();
console.log("Nonce:", nonce);
It is recommended that the state
value is a cryptographically secure, random string that does not correspond to any data used to identify users internally.
If, for any reason, the state
field needs to be used as means of identification, we recommend creating a stitch_authorization_requests
table on your backend to house relevant details about the authorization requests.
The entries contained should be easily mapped to any internal identification. Suggested columns for this table are as follows:
Column | Description |
---|---|
stitch_state | The same state value sent with the https://secure.stitch.money/connect/authorize endpoint. This column is mandatory because it allows the entry to be linked with an authorization request |
user_id | An internally determined value used to identify the users within your system. We recommend that this is a cryptographically secure value |
code_verifier | The code_verifier generated in previous steps. The code_verifier is the unhashed code_challenge |
code_challenge | The code_challenge generated in previous steps |
These columns are suggestions and only the stitch_state
is mandatory. The user_id
could be replaced with any internal
identifier and can be used in conjunction with other columns that are relevant to map.
During the authorization flow, the state
value will always be the original state
value as generated and supplied and
can be used to identify the authorization request. Once a response is received from the authorization flow, the contained
state
value can be used to map the authorization request to an entry in the stitch_authorization_requests table.
Building the Authorization URL
Once you've created a verifier challenge pair, the state, and the nonce, you can create an authorization request URL. The following JavaScript code helps you build your URL:
function buildAuthorizationUrl(
clientId,
challenge,
redirectUri,
state,
nonce,
scopes,
) {
const search = {
client_id: clientId,
code_challenge: challenge,
code_challenge_method: "S256",
redirect_uri: redirectUri,
scope: scopes.join(" "),
response_type: "code",
nonce: nonce,
state: state,
};
const searchString = Object.entries(search)
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join("&");
return `https://secure.stitch.money/connect/authorize?${searchString}`;
}
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.
https://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=J_Ug3dCQJG61Tt2Sejt3TY5ExgqemTXpEFhyFGapXwA&nonce=fim8Mwz0yHE85NOfgMD_sk3MdKzq2vQ_QnC0KUxIGT4&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:
https://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=J_Ug3dCQJG61Tt2Sejt3TY5ExgqemTXpEFhyFGapXwA&nonce=fim8Mwz0yHE85NOfgMD_sk3MdKzq2vQ_QnC0KUxIGT4&code_challenge=FVF6VCwYJ_XljGdLXVjxs6g-_QRh4_CDutLOf_oIzPw&code_challenge_method=S256
Decoding the Response
If the connect URL is correctly formed, once the user has signed in and been granted access, they'll be redirected back to theredirect_uri
.
If all went well, the value of the query string (the part of a URL delimited by a ?
) will contain the following URL encoded 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.
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_secret
needs to be used. Follow the client secret guide
to learn how to use a client secret.
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:
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_challenge generated in the previous step |
client_secret | The value of your client_secret |
Note that an authorization code can only be used once. Thereafter, it becomes invalid. The reason for 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
, clientSecret
, authorizationCode
, codeVerifier
and redirectUri
with the appropriate
values. If correctly formed, this request will return a JSON payload with the token.
clientId='test-18fbd892-3b73-43c3-a854-c6f78c681349'
authorizationCode='DH7-TaofOSCFlsQwZAeEfmap1eXPeH7nmeOMtDJhdOw'
redirectUri='https%3A%2F%2Flocalhost%3A9000%2Freturn'
codeVerifier='chy0EHtabciY0kjRzCDr113O2slAa-xY_zvdApkXxvw'
clientSecret='<your client secret>'
curl -X POST \
https://secure.stitch.money/connect/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "grant_type=authorization_code&client_id=$clientId&code=$authorizationCode&redirect_uri=$redirectUri&code_verifier=$codeVerifier&client_secret=$clientSecret"
Retrieving the Token Using JavaScript and the Fetch API
The example Javascript function below uses fetch
to retrieve the authorization code. You'll need to pass in appropriate
values for clientId
, redirectUri
, verifier
, code
, and clientSecret
to the function retrieveTokenUsingAuthorizationCode
async function retrieveTokenUsingAuthorizationCode(
clientId,
redirectUri,
verifier,
code,
clientSecret,
) {
const body = {
grant_type: "authorization_code",
client_id: clientId,
code: code,
redirect_uri: redirectUri,
code_verifier: verifier,
client_secret: clientSecret,
};
const bodyString = Object.entries(body)
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join("&");
const response = await fetch("https://secure.stitch.money/connect/token", {
method: "post",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: bodyString,
});
const responseBody = await response.json();
console.log("Tokens: ", responseBody);
return responseBody;
}
Retrieving the Token Using Postman
To get started, first import this Postman collection into your local Postman collections.
The first request in the collection is "User Token". Replace the entries in the Body tab with the appropriate values, and click send. If correctly formed, the request will return a JSON payload with the token.
A typical response body returned from the token request endpoint will look like the following:
{
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Im9TbWt2RmhqVWNia0I4MjRrek5fWkEiLCJ0eXAiOiJKV1QifQ.eyJuYmYiOjE1NzYxMzg4OTgsImV4cCI6MTU3NjEzOTE5OCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAxIiwiYXVkIjoiZjVjYzk0ZWUtZTk1MS00MGQzLWEzMWUtNzk0NjU5ODA2MjMwIiwibm9uY2UiOiJ4eXoiLCJpYXQiOjE1NzYxMzg4OTgsImF0X2hhc2giOiJqcFVVcGh5VWtaQ3diUXNZaG5zNmF3Iiwic19oYXNoIjoidW5nV3Y0OEJ6LXBCUVVEZVhhNGlJdyIsInNpZCI6InM0LWd0WXFNQUFQWkhfcy1tdjBsa3ciLCJzdWIiOiJ1c2VyL2Y1Y2M5NGVlLWU5NTEtNDBkMy1hMzFlLTc5NDY1OTgwNjIzMC9mbmIvOTdjY2M1ZmQtY2I5NS00NjViLWE0ZGMtNzYxMzk2MzNiMDdiIiwiYXV0aF90aW1lIjoxNTc2MTM3MjkxLCJpZHAiOiJsb2NhbCIsImFtciI6WyJwd2QiXX0.nVKdmdOZbNaN1lutLmvaT_nIf1kYKphXNzFHioapDigT3pNwYwmDVGQ54V40kvwJBF8PiQBzfp2hdV1S6ltJqWmnuEtLteg240FimYMdrX1sD3nQKzoMKVkbComY5lnB79QvFc5-dWRCauvZW9LY0mhnXdzRFXhb-Jv21XwzMhE_y21vPAVdPrk0jB7jyk15OZ82RwwlZYhdcBUP7Se4BMvvr4wJJEBWgxAIciFDa6gLqDyL-eInYePjhglV6KrrIHHM9C3PEd1kJJHD6uDkOf3858kS9Nr1Mnj2oNJRvpahWq8VhItsw_5JItfwaTYQoHN25Zk4A0a5Nox6weD-RA",
"access_token": "nXPh9P16drRQLnAmwy9Sf072U81KVNNa6iqduWX6kK4",
"expires_in": 900,
"token_type": "Bearer",
"refresh_token": "Vnzqys6mDyRAOz_ZOb6LOv2keOmyvOLlHEd1s6Hc2Xg",
"scope": "openid transactions accounts offline_access"
}
Note that if the user did not grant permission to have the offline_access
scope, the refresh_token
will be omitted from the response.
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:
accessToken='M6kGvDwSCBUx2oYy-CnphEdO7Ad9TnHL1fWaNaSaDSo'
curl -X POST \
https://api.stitch.money/graphql \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $accessToken" \
-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:
function queryStitchApi(accessToken) {
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Authorization", `Bearer ${accessToken}`);
var graphql = JSON.stringify({
query:
"query ListBankAccounts { user { bankAccounts { user { bankAccounts { name }}}}}",
variables: {},
});
var requestOptions = {
credentials: "include",
method: "POST",
headers: myHeaders,
body: graphql,
mode: "cors",
};
return fetch("https://api.stitch.money/graphql", requestOptions);
}
Making an Authenticated Query using Postman
To get started, first import this Postman collection into your local Postman collections.
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:
Parameter | Description |
---|---|
grant_type | For the purposes of using the refresh token, this 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_secret | The value of your client_secret |