Access on a member's behalf

How a third-party app registers, asks a member for access, and reads what the member allowed.

What this is for

A service connection belongs to a company and is made by its manager. This guide is for the other kind. That is an app that a member chooses, such as an AI assistant or a reporting tool, that acts for the member.

The app registers itself and sends the member to Norra. On Norra's consent page, the member chooses what the app gets and signs the consent with BankID. The app then trades a code for tokens. Norra speaks OAuth 2.1 with PKCE for this, and every step below is a plain HTTP request.

The examples use made-up values. Norra's own addresses are real.

Find the endpoints

The discovery document lists every endpoint, the grant types and the permissions a connection may hold:

shell
curl https://app.norra-co.com/.well-known/oauth-authorization-server

The issuer is https://app.norra-co.com. The endpoints are /oauth/register, /oauth/authorize, /oauth/token and /oauth/revoke on it.

1. Register the app

Registration is open and needs no account. Send your app's name and the addresses Norra may send the member back to:

shell
curl https://app.norra-co.com/oauth/register \
  --request POST \
  --header "Content-Type: application/json" \
  --data '{
    "client_name": "Example Assistant",
    "redirect_uris": ["https://assistant.example/callback"]
  }'

The answer is 201 with the client id. Keep it. The examples below read it from NORRA_CLIENT_ID. There is no secret: a registered app is a public client and proves itself with PKCE on each exchange.

json
{
  "client_id": "4f6c2a8e-…",
  "client_id_issued_at": 1790000000,
  "client_name": "Example Assistant",
  "redirect_uris": ["https://assistant.example/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

What registration takes:

  • client_name is required, at most 100 characters, with no control or formatting characters. The consent page shows it to the member.
  • redirect_uris holds one to ten https addresses, with no fragment and no user name or password in them. A host written in non-ASCII letters is refused. Register an internationalised name in its punycode form.
  • grant_types may name authorization_code and refresh_token, response_types may name code, and token_endpoint_auth_method may be none. Leave them out and you get those values.
  • jwks_uri, jwks, request_uris, sector_identifier_uri and software_statement are refused. Norra does not fetch anything a client points to, so it takes metadata only in the request itself.

Other fields are ignored. Registration is limited to three per address and then one every twenty seconds, ten per network, and 120 an hour for the whole deployment. Past a limit the answer is 429. Wait for the number of seconds in Retry-After.

An app that nobody has connected may be removed 30 days after it registered or was last used. Register again if client_id stops being accepted.

2. Send the member to authorize

Make a PKCE pair for each authorization. The verifier stays with your app. The challenge goes in the link.

shell
CODE_VERIFIER=$(openssl rand -base64 48 | tr -d '\n=' | tr '+/' '-_')
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" \
  | openssl dgst -sha256 -binary | openssl base64 | tr -d '\n=' | tr '+/' '-_')
STATE=$(openssl rand -hex 16)

Then open this in the member's browser, with your own values:

text
https://app.norra-co.com/oauth/authorize?response_type=code&client_id=$NORRA_CLIENT_ID&redirect_uri=https%3A%2F%2Fassistant.example%2Fcallback&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256&state=$STATE&scope=company.read%20metrics.read

The parameters:

ParameterRequiredWhat it is
response_typeYescode.
client_idYesThe id registration answered with.
redirect_uriYesOne of the addresses you registered, exactly. Required even when you registered one.
code_challengeYesThe S256 challenge: 43 characters of base64url without padding.
code_challenge_methodYesS256. plain is refused.
stateNoYour own value. It comes back unchanged. Use it to tie the answer to the request.
scopeNoPermissions, separated by spaces. Leave it out and the page offers every permission an app may hold.
companiesNoOrganisation numbers you suggest, separated by spaces or commas, at most 100. The member still picks.
resourceNoAnother Norra server the token is for, such as the mcp server. May appear more than once.

request and request_uri are refused. Send the parameters themselves.

The member signs in with BankID if they have not already, and then sees the consent page.

What the member sees

The consent page shows:

  • Your app's name, and the host it will send the member back to.
  • What the app may see. Each permission that only reads starts ticked.
  • Changes the app may ask for. These start unticked, and give an app nothing today (see What a token can do).
  • Which companies. This is one company, the companies the member ticks, or every company the member has access to. The last includes companies the member gets later. Companies you suggested start ticked. The member can name only companies they have access to.
  • Another Norra server the access also opens, when you asked for one with resource. The member cannot untick it.
  • The date the access ends.

The member can untick anything. The member then signs the consent with BankID. The app gets access when the member has signed, and not before. The connection holds what the member signed. That is at most what you asked for, and at most what the member can do.

If the member already gave your app access, the page shows what the app has and only what the new request adds. The member signs the addition with BankID. Adding to the access does not move its end date. If the request adds nothing, the member continues without a signature, and you get a new code for the same connection.

The answer at your redirect

When the member signs the consent, Norra sends the browser to your redirect with a code:

text
https://assistant.example/callback?code=nac_…&state=$STATE&iss=https%3A%2F%2Fapp.norra-co.com

Check that state is yours and that iss is https://app.norra-co.com. The code works once and for five minutes.

When the member denies, or the request is wrong, the redirect carries error and error_description instead of code, with state and iss as before:

errorWhy
access_deniedThe member did not allow access.
invalid_requestA parameter is missing, malformed or given twice, PKCE is missing or not S256, or companies is wrong.
unsupported_response_typeresponse_type is not code.
invalid_scopescope names a permission an app may not hold, or one that does not exist.
invalid_targetresource names a server this issuer does not issue tokens for.

An unknown client_id, or a redirect_uri you did not register, is not sent to any redirect. The member sees an error on Norra's page instead, and your app hears nothing.

3. Trade the code for tokens

Send the code, the same redirect address, and the verifier. Name your app with client_id in the body, and send no secret:

shell
curl https://app.norra-co.com/oauth/token \
  --request POST \
  --data grant_type=authorization_code \
  --data-urlencode "client_id=$NORRA_CLIENT_ID" \
  --data-urlencode "code=$CODE" \
  --data-urlencode "redirect_uri=https://assistant.example/callback" \
  --data-urlencode "code_verifier=$CODE_VERIFIER"

The body is application/x-www-form-urlencoded, which curl --data sends. code, redirect_uri and code_verifier are required. If you asked for a resource at authorize, name the same one here.

json
{
  "access_token": "nat_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "nrt_…",
  "scope": "company.read metrics.read"
}

scope lists what the member allowed. It can be less than you asked for.

4. Call Norra

Send the access token in the Authorization header, not in a URL. A token acts for one member on many companies, so name the company with orgnr on each call:

shell
curl "https://app.norra-co.com/v1/samples?orgnr=5590000000&source_id=7a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d" \
  --header "Authorization: Bearer $NORRA_TOKEN"

Norra records each call it answers: the connection, the company, the path and the outcome.

5. Refresh

An access token lasts an hour. Before it runs out, trade the refresh token for a new pair:

shell
curl https://app.norra-co.com/oauth/token \
  --request POST \
  --data grant_type=refresh_token \
  --data-urlencode "client_id=$NORRA_CLIENT_ID" \
  --data-urlencode "refresh_token=$NORRA_REFRESH_TOKEN"

The answer has the same shape as the code exchange, with a new refresh token. Store it and throw the old one away: a refresh token works once.

Refreshing does not move the chain's end. The last refresh token stops when the connection ends.

6. Revoke

When the member signs out of your app, revoke the token you hold:

shell
curl https://app.norra-co.com/oauth/revoke \
  --request POST \
  --data-urlencode "client_id=$NORRA_CLIENT_ID" \
  --data-urlencode "token=$NORRA_REFRESH_TOKEN"

Send either the access token or the refresh token. Norra revokes every token from the same code. The answer is 200 with no body, also when the token is unknown or already revoked. A token issued to another app is refused with 400 and unauthorized_client.

Revoking a token does not end the connection. The member ends it under Connections on their profile page in Norra, and every token stops on its next call.

Lifetimes

WhatHow long
Authorization codeFive minutes, one use.
Access tokenOne hour, or less when the connection ends sooner. expires_in says which.
Refresh tokenUntil it is used once, or the connection ends.
Connection90 days from the member's consent. Then the member gives access again.
Unused registered appMay be removed 30 days after it registered or was last used.

Norra looks up each token on every call. A revoked connection or token stops at once.

What a token can do

A token reads. It can read what the member allowed, on the companies the member chose. It can read at most what the member can read at the time of the call. When the member loses access to a company, the token loses it on the next call.

It cannot do everything a member can:

  • It does not push data. sources.ingest belongs to service connections, and an app cannot ask for it.
  • It cannot change anything today. A call that changes something is refused with 403 and permission_denied. Later an app will be able to propose a change for the member to accept.
  • It is not a BankID sign-in. A call that needs one is refused.
  • It cannot hold ledger.read.nin, ledger.read.terms, self.write, auth.login, connections.manage or any staff permission.

Errors from the OAuth endpoints

Registration, token and revocation errors are JSON in the OAuth shape:

json
{
  "error": "invalid_grant",
  "error_description": "the code or refresh token is not valid"
}

Branch on the status and on error. Log error_description and do not parse it.

StatuserrorWhat to do
400invalid_client_metadataRegistration: fix the field the description names.
400invalid_redirect_uriRegistration: a redirect address is not one Norra takes.
400invalid_requestA parameter is missing, given twice, or the body is not a form.
400invalid_grantThe code or refresh token is wrong, used, expired, or its connection ended. Send the member through authorize again.
400invalid_targetThe resource is not one the member's consent named. Send the member through authorize with it.
400unsupported_grant_typeUse authorization_code or refresh_token.
400unauthorized_clientRevocation: the token was issued to another app.
401invalid_clientThe client_id is missing or not an id, or a secret was sent. Send client_id in the body and no secret.
429temporarily_unavailableToo many requests from your address. Wait Retry-After seconds.
503temporarily_unavailableNorra issues no tokens right now. Try again later.
500server_errorTry again later.

Calls to Norra with the token answer in the shape the errors guide describes. A missing, revoked or expired token gets 401. A permission the token does not hold gets 403 with permission_denied, and a WWW-Authenticate header with error="insufficient_scope" names it. Ask the member for it through authorize: the consent page then shows only the addition.