OpenID Connect (OIDC)

TIC Identity stöder OpenID Connect för standardiserad integration med BankID. Använd vanliga OIDC-bibliotek för att integrera autentisering och få tillgång till svenska berikningsdata via UserInfo-endpointen.

Varför OIDC?
OIDC är en branschstandard som fungerar med de flesta ramverk och plattformar. Du slipper bygga egen integration och kan använda välbeprövade bibliotek.

Översikt

TIC Identity:s OIDC-implementation inkluderar:

  • Authorization Code Flow med PKCE - Säker autentisering för webbappar och native-appar
  • Discovery Document - Automatisk konfiguration via .well-known/openid-configuration
  • Svenska berikningsscopes - Hämta folkbokföring, bolagsengagemang, fastigheter och mer
  • BankID riskbedömning - Automatisk inkludering av riskindikator
  • Dubbelt dataformat - Både förenklade OIDC-claims och rådata från svenska myndigheter

Kom igång

1. Aktivera OIDC

OIDC aktiveras i portalen under fliken "OIDC" i inställningarna. När OIDC är aktiverat får du tillgång till discovery-URL:en.

2. Skapa en OIDC-klient

Varje applikation som ska använda OIDC behöver en egen klient med:

  • Client ID - Offentlig identifierare
  • Client Secret - Hemlig nyckel (visas endast vid skapande)
  • Redirect URIs - Godkända callback-URL:er
  • Post-logout Redirect URIs - Godkända URL:er för omdirigering efter utloggning (valfritt)
  • Tillåtna scopes - Vilken data klienten får begära

3. Konfigurera ditt OIDC-bibliotek

Använd discovery-URL:en för automatisk konfiguration:

https://id.tic.io/{tenant-slug}/.well-known/openid-configuration

Endpoints

Endpoint URL Beskrivning
Discovery /.well-known/openid-configuration OIDC-konfiguration och metadata
Authorization /oidc/authorize Starta autentisering
Token /oidc/token Byt authorization code mot tokens
UserInfo /oidc/userinfo Hämta användardata och berikningar
JWKS /oidc/jwks Publika nycklar för tokenvalidering
End Session /connect/endsession RP-initierad utloggning
Revocation /connect/revoke Återkalla tokens

Scopes och Claims

Scopes bestämmer vilken data som inkluderas i tokens och UserInfo-svaret. Berikningsscopes kräver att enrichment är aktiverat för din tenant.

Standardscopes

Scope Claims Beskrivning
openid sub Obligatoriskt. Subject identifier (hashad personnummer)
profile name, given_name, family_name, birthdate, locale Grundläggande profilinformation från BankID
nin nin, nin_type, nin_issuing_country Svenskt personnummer
offline_access - Får refresh token för förnyelse av access token

BankID-specifika scopes

Scope Claims Beskrivning
bankid_device bankid_device_ip IP-adress där BankID-autentisering skedde (för bedrägeridetektion)
BankID Riskbedömning
BankID:s riskindikator (risk.thirdPartyEvaluation) inkluderas automatiskt i UserInfo när tillgängligt. Möjliga värden: low, moderate, high.

Berikningsscopes

Dessa scopes triggar datahämtning från svenska myndigheter och register. Data returneras både i förenklat OIDC-format och som rådata.

Scope Källa Claims (förenklad + rådata)
address SPAR address + spar
company_roles Bolagsverket company_roles + company_roles_raw
real_estate Lantmäteriet real_estate + real_estate_raw
income Skatteverket income + income_raw
ip_intelligence IP-Intelligence ip_intelligence + ip_intelligence_raw

Bolagsverket positionTypes-koder

Fältet positionTypes i company_roles_raw innehåller en eller flera rollkoder från Bolagsverkets klassificering. En person kan ha flera koder samtidigt (t.ex. ["VD", "LE"]).

Testmiljö
I testmiljön returneras "BOLAGSENGAGEMANG" som generisk platshållarkod. I produktion returneras de specifika koderna nedan.
Kod Beskrivning
VDVerkställande direktör
VVDVice verkställande direktör
EVDExtern verkställande direktör (ej styrelsemedlem)
EVVDExtern vice verkställande direktör (ej styrelsemedlem)
SVDStällföreträdande verkställande direktör
OFOrdförande
VOFVice ordförande
LEStyrelseledamot
SUStyrelsesuppleant
VLEVerkställande ledamot
EFTExtern firmatecknare (ej styrelsemedlem)
BOBolagsman
KPKomplementär
KDKommanditdelägare
INInnehavare
Föreståndare
POProkurist
REPRepresentant
LILikvidator
LSLikvidatorssuppleant
DELGDelgivningsbar person (särskild delgivningsmottagare)
AKAktuarie
REVRevisor
REVHHuvudansvarig revisor
REVSRevisorssuppleant
REVLLekmannarevisor
REVSLLekmannarevisorssuppleant
REVTRevisor med tillstånd
REVSTAktuarie revisorssuppleant
HREVRevisor hållbarhetsrapport
HREVHHuvudansvarig revisor hållbarhetsrapport
HREVSRevisorssuppleant hållbarhetsrapport

Firmateckningsrätt

Fältet signatureDescription i company_roles_raw innehåller den registrerade firmateckningsregeln från Bolagsverket, t.ex. "Firman tecknas av styrelsen. Firman tecknas två i förening."

Rekommendation: Använd signatureDescription som primär källa för att bedöma firmateckningsrätt. Firmateckningsregler varierar mellan bolag och kan inte härledas enbart från positionTypes. Använd positionTypes för att identifiera personens roll i bolaget, och signatureDescription för att avgöra om rollen ger firmateckningsrätt.

AI-analys av firmateckningsrätt

Fältet signingAuthorityAnalysis i company_roles_raw innehåller en AI-genererad analys av firmateckningsregeln. Analysen tolkar signatureDescription och matchar reglerna mot de registrerade styrelsemedlemmarna och funktionärerna.

AI-genererat innehåll
signingAuthorityAnalysis genereras av AI och kan innehålla felaktigheter. Verifiera alltid resultatet mot den ursprungliga signatureDescription innan du fattar beslut baserat på analysen. Använd analysen som ett stöd, inte som en auktoritativ källa.
Fält Typ Beskrivning
summary string Sammanfattning på svenska av firmateckningsreglerna
rules array Tolkade firmateckningsregler med typ (alone, joint, board, other) och antal signatärer som krävs
eligiblePersons array Personer som matchar firmateckningsreglerna, med personId, personalIdentityNumber, roller och vilka regler som gäller
eligiblePersons[].canSignAlone boolean Om personen kan teckna firman ensam
30-minutersfönster för berikning
Berikningsdata är endast tillgängligt inom 30 minuter efter autentisering. Inom 30 minuter: UserInfo returnerar standardclaims + berikningsdata. Efter 30 minuter: UserInfo returnerar endast standardclaims (namn, personnummer, etc.).

Authorization Code Flow

Steg 1: Redirecta till Authorization

GET /oidc/authorize?
  response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://your-app.com/callback
  &scope=openid profile nin address company_roles
  &state=random_state_value
  &nonce=random_nonce_value
  &code_challenge=BASE64URL(SHA256(code_verifier))
  &code_challenge_method=S256

Steg 2: Användaren autentiserar med BankID

Användaren visas en BankID-autentiseringssida och genomför autentisering i BankID-appen. Efter lyckad autentisering redirectas de tillbaka.

Steg 3: Hantera callback

GET https://your-app.com/callback?
  code=AUTHORIZATION_CODE
  &state=random_state_value

Steg 4: Byt code mot tokens

curl -X POST https://id.tic.io/{tenant}/oidc/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTHORIZATION_CODE" \
  -d "redirect_uri=https://your-app.com/callback" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "code_verifier=YOUR_CODE_VERIFIER"

Svar:

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "id_token": "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
  "scope": "openid profile nin address company_roles"
}

Steg 5: Hämta UserInfo

curl https://id.tic.io/{tenant}/oidc/userinfo \
  -H "Authorization: Bearer ACCESS_TOKEN"

UserInfo-svar

UserInfo-svaret innehåller data baserat på begärda scopes. Berikningsdata returneras i två format: förenklat OIDC-format och fullständig rådata.

Konventionellt API
UserInfo inkluderar session_id som kan användas med det konventionella beriknings-API:et om du föredrar det formatet.
{
  "sub": "a1b2c3d4e5f6789012345678901234567",

  // Session ID for conventional API access
  "session_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",

  // profile scope
  "name": "Anna Andersson",
  "given_name": "Anna",
  "family_name": "Andersson",
  "birthdate": "1985-03-15",
  "locale": "sv-SE",
  "updated_at": 1706789012,

  // nin scope
  "nin": "198503151234",
  "nin_type": "PERSON",
  "nin_issuing_country": "SE",

  // bankid_device scope
  "bankid_device_ip": "192.168.1.100",

  // BankID risk (alltid inkluderat om tillgängligt)
  "risk": {
    "thirdPartyEvaluation": {
      "riskValue": "low",
      "source": "bankid"
    }
  },

  // address scope - förenklad
  "address": {
    "street_address": "Storgatan 1",
    "locality": "Stockholm",
    "postal_code": "11122",
    "country": "Sweden",
    "formatted": "Storgatan 1\n11122 Stockholm\nSweden"
  },
  // address scope - rådata från SPAR
  "spar": {
    "Person_IdNummer": "198503151234",
    "Namn_Fornamn": "Anna Maria",
    "Namn_Efternamn": "Andersson",
    "Folkbokforingsadress_SvenskAdress_Utdelningsadress1": "Storgatan 1",
    "Folkbokforingsadress_SvenskAdress_PostNr": "11122",
    "Folkbokforingsadress_SvenskAdress_Postort": "STOCKHOLM"
    // ... fler fält
  },

  // company_roles scope - förenklad
  "company_roles": [
    {
      "org_number": "5591234567",
      "company_name": "Exempel AB",
      "role": "Styrelseledamot",
      "role_code": "LE",
      "start_date": "2020-01-15"
    }
  ],
  // company_roles scope - rådata
  "company_roles_raw": [
    {
      "companyId": 12345,
      "companyRegistrationNumber": "5591234567",
      "legalName": "Exempel AB",
      "legalEntityType": "AB",
      "positionTypes": ["VD", "LE"],
      "positionDescriptions": ["Verkställande direktör", "Styrelseledamot"],
      "positionStart": "2020-01-15T00:00:00Z",
      "positionEnd": null,
      "companyStatus": "Active",
      "signatureDescription": "Firman tecknas av styrelsen. Firman tecknas två i förening.",
      "signingAuthorityAnalysis": {
        "summary": "Firman tecknas av styrelsen gemensamt, eller av två styrelseledamöter i förening.",
        "rules": [
          {
            "description": "Styrelsen gemensamt",
            "type": "board",
            "requiredSignatories": null,
            "requiredRoles": ["LE", "OF"]
          },
          {
            "description": "Två i förening",
            "type": "joint",
            "requiredSignatories": 2,
            "requiredRoles": ["LE", "OF", "VD"]
          }
        ],
        "eligiblePersons": [
          {
            "personId": 42,
            "personalIdentityNumber": "198001011234",
            "name": "Anna Svensson",
            "roles": ["Verkställande direktör", "Styrelseledamot"],
            "canSignAlone": false,
            "applicableRules": ["Två i förening"]
          }
        ]
      }
    }
  ],

  // real_estate scope - förenklad
  "real_estate": [
    {
      "property_designation": "STOCKHOLM VASASTAN 1:23",
      "municipality": "STOCKHOLM",
      "ownership_share": "1/2",
      "acquisition_date": "2018-06-01"
    }
  ],
  // real_estate scope - rådata
  "real_estate_raw": [
    {
      "FastighetObjektIdentitet": "abc123",
      "Registerbeteckning": "STOCKHOLM VASASTAN 1:23",
      "AndelTaljare": 1,
      "AndelNamnare": 2,
      "OwnershipStartDate": "2018-06-01T00:00:00Z"
    }
  ],

  // income scope - förenklad (senaste år)
  "income": {
    "year": 2023,
    "employment_income": 450000,
    "capital_income": 25000,
    "business_income": 0,
    "total_income": 475000
  },
  // income scope - rådata (alla år)
  "income_raw": [
    {
      "AR_DEKL": 2023,
      "INK_TJ": 450000,
      "OSKOTT_KAP": 25000
      // ... fler fält
    }
  ],

  // ip_intelligence scope - förenklad
  "ip_intelligence": {
    "ip_address": "85.123.45.67",
    "country": "Sweden",
    "city": "Stockholm",
    "is_vpn": false,
    "is_proxy": false,
    "is_tor": false,
    "risk_score": 0.05
  },
  // ip_intelligence scope - rådata
  "ip_intelligence_raw": {
    "InitiatingIp": {
      "IpAddress": "85.123.45.67",
      "CountryCode": "SE",
      "CountryName": "Sweden",
      "ConfidenceScore": 0,
      "IsLikelyVpn": false,
      "IsTor": false
    },
    "OverallRisk": {
      "Level": "Low",
      "Score": 5
    }
  }
}

Exempelintegrationer

ASP.NET Core

// Program.cs
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
    options.Authority = "https://id.tic.io/your-tenant";
    options.ClientId = "your-client-id";
    options.ClientSecret = "your-client-secret";
    options.ResponseType = "code";
    options.SaveTokens = true;
    options.GetClaimsFromUserInfoEndpoint = true;

    // Lägg till önskade scopes
    options.Scope.Add("profile");
    options.Scope.Add("nin");
    options.Scope.Add("address");
    options.Scope.Add("company_roles");
});

Next.js (NextAuth.js)

// pages/api/auth/[...nextauth].js
import NextAuth from "next-auth";

export default NextAuth({
  providers: [
    {
      id: "tic-identity",
      name: "TIC Identity",
      type: "oauth",
      wellKnown: "https://id.tic.io/your-tenant/.well-known/openid-configuration",
      clientId: process.env.TIC_CLIENT_ID,
      clientSecret: process.env.TIC_CLIENT_SECRET,
      authorization: {
        params: {
          scope: "openid profile nin address company_roles"
        }
      },
      idToken: true,
      profile(profile) {
        return {
          id: profile.sub,
          name: profile.name,
          personalNumber: profile.nin,
        };
      },
    },
  ],
});

Python (Authlib)

from authlib.integrations.flask_client import OAuth

oauth = OAuth(app)
oauth.register(
    name='tic_identity',
    client_id='your-client-id',
    client_secret='your-client-secret',
    server_metadata_url='https://id.tic.io/your-tenant/.well-known/openid-configuration',
    client_kwargs={
        'scope': 'openid profile nin address company_roles'
    }
)

@app.route('/login')
def login():
    redirect_uri = url_for('callback', _external=True)
    return oauth.tic_identity.authorize_redirect(redirect_uri)

@app.route('/callback')
def callback():
    token = oauth.tic_identity.authorize_access_token()
    userinfo = oauth.tic_identity.userinfo()
    # userinfo innehåller nu all begärd data
    return f"Välkommen {userinfo['name']}"

Token-livstider

Standardlivstider kan justeras i portalen:

Token Standard Intervall
Access Token 1 timme 5 min - 24 timmar
ID Token 1 timme 5 min - 24 timmar
Refresh Token 30 dagar 1 timme - 1 år
Authorization Code 5 minuter Fast

Utloggning (RP-Initiated Logout)

För att logga ut en användare, redirecta till End Session-endpointen med ID-token som hint. Om en registrerad post_logout_redirect_uri anges redirectas användaren dit efter utloggning.

GET /connect/endsession?
  id_token_hint=eyJhbGciOiJSUzI1NiIs...
  &post_logout_redirect_uri=https://your-app.com/logged-out
  &state=optional_state_value
Parameter Obligatorisk Beskrivning
id_token_hint Ja* ID-token som erhölls vid inloggning. Krävs om post_logout_redirect_uri anges.
post_logout_redirect_uri Nej URL dit användaren redirectas efter utloggning. Måste vara registrerad på klienten.
state Nej Godtyckligt värde som skickas tillbaka som query-parameter vid redirect.

Om ingen post_logout_redirect_uri anges visas en bekräftelsesida. Post-logout redirect URIs konfigureras per klient i portalen.

Säkerhet

PKCE (Proof Key for Code Exchange)

PKCE är obligatoriskt för alla klienter och skyddar mot authorization code interception-attacker. Använd code_challenge_method=S256 för maximal säkerhet.

Tokenvalidering

Validera alltid ID-tokens och access tokens i din backend:

  • Verifiera signaturen mot JWKS-endpointen
  • Kontrollera iss (issuer) och aud (audience)
  • Verifiera att exp (expiration) inte har passerat
  • Kontrollera nonce om du använde det i authorization-request

Felhantering

OIDC-fel returneras enligt OAuth 2.0-specifikationen:

Felkod Beskrivning
invalid_request Ogiltig eller saknad parameter
unauthorized_client Klienten får inte använda denna grant type
access_denied Användaren avbröt eller nekade åtkomst
invalid_scope Begärt scope är ogiltigt eller inte tillåtet
invalid_grant Authorization code är ogiltig eller har redan använts
invalid_client Client ID eller secret är felaktig

Nästa steg