User AuthenticationDeprecated
This section includes information around Stitch products and features that are now deprecated. Deprecated aspects of Stitch's plaform are no longer being supported or improved upon.
If you are currently using a deprecated Stitch product, please consider upgrading to one of our newer and equivalent offerings.
To initiate payment using the userInitiatePayment
mutation, a user token is required. To
acquire a user token, two steps are involved:
- Obtain an authorization code.
- Use the authorization code to obtain the user and refresh tokens.
Obtaining an Authorization Code
The first step in the process entails passing a few query string parameters to the endpoint https://secure.stitch.money/connect/authorize
.
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.
For LinkPay, you'll at least need the openid
, transactions
,
accounts
,
accountholders
, balances
scopes. Should your application
also require refresh_tokens, you can add the
offline_access
scope to the list of requested scopes.
Parameter | Description |
---|---|
client_id | The unique ID of the client you generated. |
scope | A non-empty, space-separated list of requested scopes. LinkPay requires these scopes openid transactions accounts balances accountholders offline_access |
response_type | Instructs Stitch to return an authorization code. This should always have a value of code . |
redirect_uri | One of the whitelisted URLs in the client configuration. After login, the user will be redirected back to this URL. The redirect_uri is whitelisted to prevent open redirect attacks. |
nonce | Mitigates replay attacks. The value should be a cryptographically secure, random string between 32 and 300 characters in length. |
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. Valid characters for
the code_verifier
are 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 is {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 = generateCodeChallenge(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
parameters are required to make an authorization request. The state
parameter is required to
prevent Cross-Site Request Forgery (CSRF), while the nonce
parameter is required to prevent replay attacks.
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.
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 unhashed code_challenge generated in previous step. |
code_challenge | The hashed code_challenge generated in previous step. |
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 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}`;
}
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.
https://secure.stitch.money/connect/authorize?client_id=test-18fbd892-3b73-43c3-a854-c6f78c681349&scope=openid%20transactions%20accounts%20balances%20accountholders%20offline_access%20paymentinitiationrequest&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 transactions accounts balances accountholders offline_access&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 authorization request URL is correctly formed, once the user has signed in and grants access, they'll be
redirected back to the redirect_uri
. 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) |
Please note that the authorization code expires after 5 minutes. Once it's expired, you'll need to fetch a new one
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 (e.g. a user chooses to cancel signing in), then the response will include
only the state
parameter above.
Retrieving a User Token
The next step entails retrieving a user 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 https://secure.stitch.money/connect/token endpoint, with the content
type application/x-www-form-urlencoded
and the following fields in the body:
Parameter | Description |
---|---|
grant_type | For the purposes of retrieving a user token, should always be authorization_code |
client_id | This is the unique ID of the client you generated |
client_secret | The value of your client_secret |
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 |
An authorization code can only be used once. Thereafter, it becomes invalid to mitigate opportunities for replay attacks.
Retrieving the Token Using Postman
To get started, first import this Postman collection into your Postman client.
The request to edit in the collection is Retrieve 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.
Retrieving the Token Using JavaScript
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;
}
Response Body
A typical response body returned from the token request endpoint will look like the following sample response:
{
"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 balances accountholders offline_access"
}
If the offline_access
scope was not part of the requested scopes in the authorization request URL, the refresh_token
will be omitted from above 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 |
Using Refresh Tokens
Eventually, the user token you've been using will expire, and
API calls will start returning a status code of 401
. If that is the case, and you had added the offline_access
scope during
linking, you obtained a refresh token which can generate a new user token (and a new refresh token as well).
This process also uses the https://secure.stitch.money/connect/token
endpoint and the content type application/x-www-form-urlencoded
.
The call should have the following parameters:
Parameter | Description |
---|---|
grant_type | For the purposes of retrieving a 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 |
Retrieving the Token Using JavaScript
The example Javascript function below uses fetch
to get a new user and refresh token pair. You'll need to pass in appropriate
values for clientId
, refreshToken
, and clientSecret
to the function retrieveTokenUsingAuthorizationCode
async function refreshUserToken(clientId, refreshToken, clientSecret) {
const body = {
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: clientId,
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;
}