Getting Access Tokens Using Client Secret

Generating Tokens for RKVST

Having completed the steps at Application Registration, and taken note of the Application ID and the secret, a token can be obtained with the following command.

Replace ${API_APP_ID} with the application id, and ${API_APP_SECRET} with your secret from the application registration; ${FQDN} is the FQDN for your Jitsuin RKVST.

${TENANT} is your directory id, see how to locate your Tenant here.

$ RESPONSE=$(curl \${TENANT}/oauth2/token\
    --data-urlencode "grant_type=client_credentials" \
    --data-urlencode "client_id=${API_APP_ID}" \
    --data-urlencode "client_secret=${API_APP_SECRET}" \
    --data-urlencode "resource=https://${FQDN}")

$ TOKEN=$(echo -n $RESPONSE | jq .access_token | tr -d '"')

Testing Access

To confirm access token configuration, use the shell command (above) to obtain an access token. The response is json structured data. The token is found in the access_token field. It is a base64 encoded JSON Web Token.

The header and payload of the TOKEN can be examined with the following commands.

# Header
echo -n $TOKEN | cut -d '.' -f 1 | base64 -D

# Payload
echo -n $TOKEN | cut -d '.' -f 2 | base64 -D
Note: Decoding tokens with an online service exposes your RKVST until you delete the test secret.

The following python script demonstrates how to safely obtain and verify a token.

  1. Run the script like this, the example requires Python 3:
python3 -t ${TENANT} -c ${API_APP_ID} -s ${API_APP_SECRET} -f ${FQDN}

Copy the following python code to

#!/usr/bin/env python3

# REQUIRES Python 3.6
import sys
import argparse
import subprocess as sp
import urllib.parse
import base64
import json
import calendar
import datetime

    import jwcrypto
    import jwcrypto.jwk
    import jwcrypto.jwt
except ImportError:

def run():
    p = argparse.ArgumentParser( description=__doc__)

    p.add_argument("-T", "--token")
    p.add_argument("-t", "--tenant")
    p.add_argument("-c", "--client-id")
    p.add_argument("-s", "--client-secret")
    p.add_argument("-f", "--fqdn")

    args = p.parse_args()

    # Support checking a token provided 'as is' and also fetching and checking
    # a token using the expected customer configuration items

    token = args.token
    if token is None:
        secret = urllib.parse.quote(args.client_secret)
        resource = urllib.parse.quote("https://" + args.fqdn)

        data = f"grant_type=client_credentials&client_id={args.client_id}"
        data += f"&client_secret={secret}&resource={resource}"

        cmd = [
            "curl", "-X", "POST",
            "-HContent-Type: application/x-www-form-urlencoded",
            "-d", data]

        # Avoid the unpleasant curl output
        cp =, stdout=sp.PIPE, stderr=sp.PIPE, check=True)
        token = cp.stdout.decode()
        jdoc = json.loads(token)
        token = jdoc["access_token"]

    header, payload, *sig = token.split('.')

    header = json.loads(base64.b64decode(header + "===").decode())

    payload = json.loads(base64.b64decode(payload + "===").decode())
    print(json.dumps(payload, indent=4, sort_keys=True))

    # Check that the 'aud' field matches the resource
    if args.fqdn and 'https://' + args.fqdn != payload["aud"]:
        print("Missing or unexepected aud", file=sys.stderr)
        return -1

    # Check that its issued by the expected tenancy
    if args.tenant and args.tenant not in payload["iss"]:
        print("Unexepected directory id in issuer (iss)", file=sys.stderr)

    # Check the Jitsuin RKVST roles are present
    roles = payload["roles"]
    if "archivist_administrator" not in roles and "guest" not in roles:
        print("Token is missing the required roles", file=sys.stderr)
        return -1

    # Check the freshly issued token has not expired and that the issue time is
    # sensible
    iat = int(payload["iat"])
    exp = int(payload["exp"])
    now = calendar.timegm(datetime.datetime.utcnow().utctimetuple())

    if now < iat:
        print(f"iat before 'now'. iat={iat}, now={now}", file=sys.stderr)
        return -1
    if now >= exp:
            f"now after 'exp', token expired "
            f"or invalid. now={now}, exp={exp}", file=sys.stderr)
        return -1

    # Get the IdP Open ID configuration
    cmd = [
        "curl", "-HAccept: application/json",
    cp =, stdout=sp.PIPE, stderr=sp.PIPE, check=True)

    oidconf = json.loads(cp.stdout.decode())

    # Fetch the keys for verification
    cmd = ["curl", "-HAccept: application/json", f"{oidconf['jwks_uri']}"]
    cp =, stdout=sp.PIPE, stderr=sp.PIPE, check=True)

    jwks = json.loads(cp.stdout.decode())
    key = None
    for k in jwks["keys"]:
        if k["kid"] == header["kid"]:
            key = k
    if key is None:
            "Failed to find token verification key at issuer", file=sys.stderr)
        return -1

    if verify_token is False:
        print("Please install jwcrypto to verify your token")
        return 0

    jwk = jwcrypto.jwk.JWK(**key)
    jwt = jwcrypto.jwt.JWT()
    # If there is any problem with the token, this function will raise an
    # exception.
    jwt.deserialize(token, key=jwk)

    return 0

if __name__ == "__main__":
    except json.decoder.JSONDecodeError as e:
        print(f"json decoding error {str(e)}")
    except sp.CalledProcessError as cpe:
        print(cpe.output, file=sys.stderr)
    except KeyError as e:
        print(f"expected key missing {str(e)}", file=sys.stderr)
    except ValueError as e:
        print(str(e), file=sys.stderr)
    except Exception as e:
        print(str(e), file=sys.stderr)

Delete the test secret once this test is completed.

Certificate based assertion of identity is fully supported. See client_assertion_type and client_assertion in the official Azure documentation

Edit this page on GitHub