Getting Access Tokens (client secret)

Having completed the steps at Create an Application registration, and taken note of its 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 Archivist. ${TENANT} is your directory id, see Open Azure Active Directory

$ RESPONSE=$(curl \
  https://login.microsoftonline.com/${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 Archivist until you delete the test secret.

The following python script demonstrates how to safely obtain and verify a token. The example requires python 3.6. Run the script like this:

python3 check-token.py -t ${TENANT} -c ${API_APP_ID} -s ${API_APP_SECRET} -f ${FQDN}

Copy the following python code to check-token.py

#!/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

verify_token=True
try:
    import jwcrypto
    import jwcrypto.jwk
    import jwcrypto.jwt
except ImportError:
    verify_token=False


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",
            f"https://login.microsoftonline.com/{args.tenant}/oauth2/token",
            "-d", data]

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

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

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

    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 Archivist 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:
        print(
            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",
        f"{payload['iss']}/.well-known/openid-configuration"]
    cp = sp.run(cmd, 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 = sp.run(cmd, 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
            break
    if key is None:
        print(
            "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__":
    try:
        sys.exit(run())
    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)
    sys.exit(-1)

Delete the test secret once this test is completed.

Note

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