# Sign customers in and out

> Sign customers in with any OpenID Connect library, read the ID token, sign them out properly, and handle every way a sign-in can be refused.

Source: https://docs.trustaige.com/customer-credentials/sign-in

Replace these placeholders with your organization's own values:

- `<your-client-id>`: App client ID, for example `q3Jx8vN2mZ0bT5sLwK7yHc4dR1fGaE9uPiO6nVjXkMs`. On the app’s page in the hub, under Credentials.
- `<your-app-url>`: Your app’s address, for example `https://app.example.com`. Scheme and host, no path: where your app runs.

Signing in is a standard OpenID Connect sign-in against your organization's
issuer. Nothing here is specific to Trustaige except what the ID token carries
and the wording of refusals.

## Sign in

Use any OpenID Connect library, with the authorization code flow and PKCE.
Point it at your issuer (`TRUSTAIGE_ISSUER`); discovery gives it every endpoint
and your organization's own signing keys (ES256). Request the `openid` scope.

```go title="Go, with golang.org/x/oauth2 and go-oidc"
provider, _ := oidc.NewProvider(ctx, os.Getenv("TRUSTAIGE_ISSUER"))
config := oauth2.Config{
    ClientID:     os.Getenv("TRUSTAIGE_CLIENT_ID"),
    ClientSecret: os.Getenv("TRUSTAIGE_CLIENT_SECRET"),
    RedirectURL:  "<your-app-url>/auth/callback",
    Endpoint:     provider.Endpoint(),
    Scopes:       []string{oidc.ScopeOpenID},
}
verifier := provider.Verifier(&oidc.Config{ClientID: config.ClientID})
```

### The callback

The callback is where most integrations go wrong, so here is the whole of it.
Keep the state, nonce and PKCE verifier in a short-lived cookie when the
sign-in starts. On the way back, check each one, exchange the code, verify the
token, and look the customer up by `sub`.

```go title="Go — the callback"
func (a *Auth) Callback(w http.ResponseWriter, r *http.Request) {
    if e := r.URL.Query().Get("error"); e != "" {
        // error_description is attacker-controlled: choose your own message.
        reason := "cancelled"
        if e == "access_denied" {
            d := r.URL.Query().Get("error_description")
            switch {
            case strings.Contains(d, "don't have an account"):
                reason = "no_account"
            case strings.Contains(d, "cancelled the sign-in"), strings.Contains(d, "denied the authorization request"):
                reason = "cancelled"
            default:
                reason = "inactive"
            }
        }
        a.askForPasskeyNextTime(w) // prompt=login on the next attempt
        http.Redirect(w, r, "/signin?error="+reason, http.StatusFound)
        return
    }
    txn, err := a.readAndClearTxnCookie(w, r) // state, nonce, PKCE verifier
    if err != nil || r.URL.Query().Get("state") != txn.State {
        a.fail(w, r, "mismatch")
        return
    }
    token, err := a.oauth2.Exchange(r.Context(), r.URL.Query().Get("code"),
        oauth2.VerifierOption(txn.Verifier))
    if err != nil {
        a.fail(w, r, "failed")
        return
    }
    raw, _ := token.Extra("id_token").(string)
    idToken, err := a.verifier.Verify(r.Context(), raw) // signature, iss, aud, exp
    if err != nil || idToken.Nonce != txn.Nonce {
        a.fail(w, r, "mismatch")
        return
    }
    customer, err := a.store.CustomerBySubject(r.Context(), idToken.Subject)
    if err != nil {
        a.fail(w, r, "no_account")
        return
    }
    if customer.EmailVerifiedAt == nil {
        a.store.MarkEmailVerified(r.Context(), customer.ID) // first sign-in from your link
    }
    a.startSession(w, customer.ID, raw) // keep the ID token for sign-out
    a.clearAskForPasskey(w)
    http.Redirect(w, r, txn.ReturnTo, http.StatusFound)
}
```

### What the ID token carries

| Claim | Meaning |
|---|---|
| `sub` | This person for your organization: the subject you were given. Look your customer up by it. |
| `credential.type` | The credential that admitted them, such as `Customer`. If they hold more than one your app admits, Trustaige asks which, and this is the one they chose. |
| `credential.claims` | The values you chose at offer, such as `{ "tier": "standard" }`. |
| `credential.document_number` | This particular credential. It changes when the credential renews: never key an account on it. |
| `credential.issuer` | The organization that issued it. Yours, unless a collaborator shared it with you. |
| `credential.expires_at` | When it ends, in seconds since the epoch. Absent if it doesn't end. |

There's no email or name in the token: you already hold them, and Trustaige
doesn't. There's no password, so there's nothing to reset. A person who loses
their device recovers their own identity with Trustaige; you're not involved.
Paste a token into the [token decoder](/tools/token-decoder) to see it.

> **Note:** There's no refresh token. Your own session lasts as long as you decide; each new
> sign-in is checked against the credential, so a suspension or revocation takes
> effect at the person's next sign-in.

## Sign out

End your own session, then send the browser to the end-session endpoint from
discovery, with the ID token as a hint and your registered post-logout URI.

```http title="Redirect"
GET https://id.trustaige.com/oauth2/logout
  ?client_id=<your-client-id>
  &id_token_hint=<the ID token>
  &post_logout_redirect_uri=<your-app-url>/
```

> **Ask for the passkey again after a sign-out:** Signing out of your app doesn't sign the person out of Trustaige: that session
> is theirs, not yours. So on the first sign-in after a sign-out, add
> `prompt=login` to the authorization request. Trustaige then asks for the
> passkey even if the person is still signed in there, so nobody who uses the
> computer next walks into their account. Remember a sign-out with a cookie and
> add the parameter once.

## When sign-in is refused

A refused sign-in comes back to your callback as a standard OAuth error, with
no code and no token. The `error_description` arrives in your own URL, where
anyone can put text: use it to choose between your own messages, and never show
it.

| `error` | Means | Tell the person |
|---|---|---|
| `access_denied`, "don't have an account" | The passkey belongs to someone with no account with you | There's no account for this passkey. Create one, or use a different passkey. |
| `access_denied`, "no longer active" | Their credential is suspended, revoked or expired | Their account isn't active, and how to reach you |
| `access_denied`, "don't hold a credential this application accepts" | None of their credentials is one you accept, or a collaborator stopped sharing it | They can't sign in here with what they hold |
| `access_denied`, "denied the authorization request" | They chose not to share a collaborator's credential with you | Nothing to show; they can try again |
| `access_denied`, "cancelled the sign-in" | They went back from the Trustaige sign-in card to you | Nothing to show; offer sign-in and sign-up as usual |
| any other error | They cancelled, or something failed | Sign-in didn't complete. Try again. |

After any refusal, ask for the passkey on the next attempt (`prompt=login`), so
the person can choose a different passkey instead of being refused again by the
same Trustaige session.

Next: [after sign-up](/customer-credentials/lifecycle).
