# Machine-to-Machine Access with the Client Credentials Grant

## Introduction

A nightly job reads orders from an API. A billing service asks a pricing service for a quote. No user is involved in either call, so there is nobody to send to a login page. The calling service proves who it is with its own credentials and gets an [access token](https://www.abblix.com/en/docs/glossary-overview#access-token) for itself. That is the [client credentials grant](https://www.abblix.com/en/docs/glossary-overview#client-credentials-grant) of [RFC 6749, section 4.4](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4).

This guide sets it up end to end with two projects from the [Getting Started repository](https://github.com/Abblix/Oidc.Server.GettingStarted):

- `OpenIDProviderApp.MinimalApi`, the provider, on `https://localhost:5006`. It already registers a client for this grant, `console_client`.
- `ApiSample`, the protected API, on `https://localhost:5004`. It accepts a request only when its access token carries the `weather` scope.

By the end, a token request returns a token, and the API answers 200 to a call carrying it. Along the way you will also see three near-miss requests and the refusal each one gets.

## Register the API as a resource

The provider has to know about the API before it can issue a token for it. Declare it as a resource: its address and the [scopes](https://www.abblix.com/en/docs/glossary-overview#scope) it accepts. The sample does this in `Program.cs`:

```csharp
builder.Services.AddOidcServices(options =>
{
    options.Resources =
    [
        new ResourceDefinition(new Uri("https://localhost:5004", UriKind.Absolute), new ScopeDefinition("weather")),
    ];
});
```

The address becomes the audience (`aud`) of every token issued for this API, and the API checks that value, so it has to match exactly what the API expects. The `weather` scope belongs to this resource, which matters when the token is requested: see [Name the resource in the request](#name-the-resource-in-the-request).

## Register the calling service as a client

The sample already registers `console_client` in the `Oidc` section of `appsettings.json`, which the provider binds into its options. A registration for a real service has the same shape and adds `AllowedScopes`:

```json
{
  "ClientId": "console_client",
  "ClientSecrets": [ { "Sha512Hash": "vSsar3708Jvp9Szi2NWZZ02Bqp1qRCFpbcTZPdBhnWgs5WtNZKnvCXdhztmeD2cmW192CF5bDufKRpayrW/isg==" } ],
  "TokenEndpointAuthMethod": "client_secret_post",
  "AllowedGrantTypes": [ "client_credentials" ],
  "AllowedScopes": [ "weather" ]
}
```

What each setting controls:

- `ClientSecrets` holds the SHA-512 hash of the [client secret](https://www.abblix.com/en/docs/glossary-overview#client-secret), base64-encoded, never the secret itself. The hash above is the sample's test secret, `secret`, and it is published in a public repository, so never deploy it. Generate a random secret with `openssl rand -base64 32` and hash it with `printf %s "$SECRET" | openssl dgst -sha512 -binary | openssl base64 -A`. A single SHA-512 hash is safe only for a random secret like this one, never for a password. The calling service keeps the secret itself in a secret store rather than in its `appsettings.json`.
- `ClientSecrets` is a list so that a secret can be rotated without downtime: add the new hash next to the old one (each entry can carry an `ExpiresAt`), move the service to the new secret, then remove the old entry.
- `TokenEndpointAuthMethod` is enforced. A client registered with `client_secret_post` sends its credentials in the form body; the same credentials sent in an HTTP Basic header are refused with `invalid_client`. The library's default is `client_secret_basic`, the method [RFC 6749, section 2.3.1](https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1) prefers and most client libraries send. The sample registers `client_secret_post` only so that the `curl` call below stays readable; for a real service, register `client_secret_basic` or leave the setting out.
- `AllowedGrantTypes` limits the client to this grant. A service has no use for the authorization code grant, so it should not have it.
- `AllowedScopes` limits which scopes the client may ask for. Leave it out and the client may ask for any scope the provider knows; the sample's `console_client` leaves it out. It is also the only per-client boundary between your APIs: the provider issues a token for any registered resource a client names, so a client without `AllowedScopes` can reach every scope of every API this provider protects.

## Request a token

The service posts to the [token endpoint](https://www.abblix.com/en/docs/glossary-overview#token-endpoint), which the provider names in its [discovery document](https://www.abblix.com/en/docs/discovery-and-well-known-endpoints) (`/.well-known/openid-configuration`). You will run this call in [Run it](#run-it), once both projects are started; for now, read what it sends:

```bash
curl -X POST https://localhost:5006/connect/token \
  -d grant_type=client_credentials \
  -d client_id=console_client \
  -d client_secret=secret \
  -d scope=weather \
  -d resource=https://localhost:5004
```

The response carries the token and how long it lasts:

```json
{
  "access_token": "eyJ0eXAiOiJhdCtqd3Qi...",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 600,
  "refresh_token": null,
  "scope": "weather",
  "id_token": null,
  "authorization_details": null
}
```

The token is a JWT of type `at+jwt`, the [JWT profile for access tokens](https://www.abblix.com/en/docs/glossary-overview#jwt-profile-for-oauth-2-0-access-tokens) of [RFC 9068](https://datatracker.ietf.org/doc/html/rfc9068). Decode its payload and you will find `aud` set to `https://localhost:5004`, `scope` set to `weather`, and both `sub` and `client_id` set to `console_client`, because the service is acting for itself. No refresh token comes with it: when the token expires, the service asks for a new one the same way.

Its lifetime comes from the client's `AccessTokenExpiresIn`, ten minutes unless you set it. That lifetime is also how long a leaked token stays usable, because a self-contained token cannot be recalled: disabling the client or rotating its secret does not stop a token already issued. The API adds the `JwtBearer` handler's default clock skew of five minutes on top, so a ten-minute token can be accepted for up to fifteen. Keep `AccessTokenExpiresIn` short, and lower `TokenValidationParameters.ClockSkew` in the API if that window matters.

The calling service should not request a new token for every call. Cache it per scope and resource, refresh it about a minute before `expires_in` runs out, and on a `401` from the API fetch a new one once and retry.

## Name the resource in the request

The `resource` parameter comes from [Resource Indicators for OAuth 2.0](https://www.abblix.com/en/docs/glossary-overview#resource-indicators-for-oauth-2-0) ([RFC 8707](https://datatracker.ietf.org/doc/html/rfc8707)). It tells the provider which API the token is for. Abblix OIDC Server also makes a resource's scopes available only to a request that names the resource; other providers may infer the audience from the scope instead. Each of these requests looks almost right and fails in its own way:

- Scope without the resource. The provider answers `400` with `invalid_scope` and "The scope is not available": `weather` belongs to the resource, and a request that names no resource has no access to the resource's scopes.
- The resource without the scope. The token is issued for `https://localhost:5004` with an empty `scope`, and the API answers `403`: the caller is authenticated, but its token grants nothing the endpoint requires. That empty-scope token is still valid, with the right audience, for any client of this provider, so an API that only requires an authenticated caller lets every one of them in. Every endpoint has to require a scope as well.
- Neither. The token's audience falls back to the provider's own issuer, and the API answers `401` with `invalid_token`, because the audience is not the one it expects. A provider with a single API can set `OidcOptions.DefaultResourceIndicator` to that API's address; a request that names no resource then gets it as its audience, though a resource scope still needs the resource to be named.

## Validate the token in the API

`ApiSample` validates tokens with the standard `JwtBearer` handler, configured in `appsettings.Development.json`:

```json
"JwtBearerAuthentication": {
  "Authority": "https://localhost:5001",
  "MapInboundClaims": false,
  "TokenValidationParameters": {
    "ValidTypes": [ "at+jwt" ],
    "ValidAudience": "https://localhost:5004",
    "ValidIssuer": "https://localhost:5001"
  }
}
```

- `Authority` is where the handler fetches the provider's discovery document and signing keys. The file points it at the MVC provider on port 5001; [Run it](#run-it) points it at the Minimal API provider instead.
- `ValidIssuer` must equal the token's `iss`, the provider's issuer. Like `Authority`, it names port 5001 in the file and is moved to 5006 in [Run it](#run-it).
- `ValidTypes` accepts access tokens only. An ID token signed by the same provider is refused, even though its signature is valid.
- `ValidAudience` is the resource address registered above. This check is what stops a token issued for another API from being accepted here.
- `MapInboundClaims` set to `false` keeps the claim names as the token carries them, so the scope is read from a claim called `scope`.

The scope check is an authorization policy. The `scope` claim is a single string of space-separated values, so the policy splits it before looking for `weather`:

```csharp
builder.Services.AddAuthorization(options => options.AddPolicy("WeatherApi", policy =>
{
    policy.RequireAuthenticatedUser();
    policy.RequireAssertion(context =>
    {
        var scope = context.User.FindFirstValue("scope")?.Split(' ', StringSplitOptions.RemoveEmptyEntries);
        return scope is not null && scope.Contains("weather", StringComparer.Ordinal);
    });
}));
```

The sample applies this policy to `/weatherforecast` with `RequireAuthorization("WeatherApi")`. A check of the whole string for equality with `weather` works until the day a client asks for two scopes, and then refuses a token that should pass.

## Run it

Both projects listen on HTTPS with the ASP.NET Core development certificate, so trust it once with `dotnet dev-certs https --trust`, or `curl` refuses the connection. Clone the [Getting Started repository](https://github.com/Abblix/Oidc.Server.GettingStarted) and, from its root, start the provider:

```bash
dotnet run --project OpenIDProviderApp.MinimalApi
```

In a second terminal, start the API with its authority and issuer pointed at that provider. Environment variables override `appsettings.Development.json`, so no file needs editing:

```bash
JwtBearerAuthentication__Authority=https://localhost:5006 \
JwtBearerAuthentication__TokenValidationParameters__ValidIssuer=https://localhost:5006 \
dotnet run --project ApiSample
```

In PowerShell, set both variables and then start the API:

```text
$env:JwtBearerAuthentication__Authority = "https://localhost:5006"
$env:JwtBearerAuthentication__TokenValidationParameters__ValidIssuer = "https://localhost:5006"
dotnet run --project ApiSample
```

Request a token with the `curl` call from [Request a token](#request-a-token), then call the API with it:

```bash
curl https://localhost:5004/weatherforecast -H "Authorization: Bearer <access_token>"
```

The API answers `200` with a JSON forecast. The same call without the header answers `401`. In PowerShell, run these calls as `curl.exe` with all the arguments on one line, since the backslash line breaks above are bash syntax.

## Troubleshooting

### The API answers 401 to a fresh token

Decode the token and check `aud`. If it is the provider's address rather than `https://localhost:5004`, the request left out `resource=https://localhost:5004`. If no token reached the API at all, check the `Authorization: Bearer` header.

### The API answers 401 after the provider was restarted

The sample provider generates a new signing key every time it starts, so tokens issued after a restart carry a key the API has not cached. The API fetches the provider's key set again after such a failure, but no more than once every five minutes, so the first call fails and later ones pass; after several restarts in a row, it can take up to five minutes. Restarting the API is the quickest way back. In production, the provider publishes a new key before it signs with it and keeps the old one published until the tokens it signed have expired, which is one reason to keep signing keys outside the process: [Persisting JWT Signing Keys in Production](https://www.abblix.com/en/docs/signing-key-persistence).

### invalid_client from the token endpoint

The secret is wrong, or the credentials are sent in a way the registration does not allow. A client registered with `client_secret_post` sends `client_id` and `client_secret` in the form body, as the `curl` call above does.

### invalid_scope from the token endpoint

The provider checks the client's `AllowedScopes` first. "The scope is not allowed for this client" means the scope is not in that list, and for a client that lists its scopes this also covers a misspelled scope. "The scope is not available" means the scope passed that check but the provider cannot grant it: a resource scope requested without its resource, or a scope the provider does not know at all.

### 403 from the API

The token is valid but its `scope` does not contain `weather`. Check that the request sent `scope=weather` and that the token response echoes it.

## Next steps

- A shared secret is the simplest [client authentication](https://www.abblix.com/en/docs/glossary-overview#client-authentication) method and the easiest to leak. For services you control, consider [private_key_jwt](https://www.abblix.com/en/docs/glossary-overview#private-key-jwt) or [mutual TLS](https://www.abblix.com/en/docs/glossary-overview#mutual-tls-client-authentication), where the client proves possession of a key and no secret travels.
- The token itself is a bearer token: whoever holds it can use it until it expires. Binding it to the client's key with mutual TLS or [DPoP](https://www.abblix.com/en/docs/glossary-overview#oauth-2-0-dpop), both of which the provider supports, makes a stolen token useless, as [OAuth 2.0 Security Best Current Practice](https://datatracker.ietf.org/doc/html/rfc9700) recommends.
- [Production Hardening Checklist](https://www.abblix.com/en/docs/production-hardening-checklist) covers what to change before this provider faces real traffic.
- [Securing a React SPA with the BFF Pattern](https://www.abblix.com/en/docs/react-spa-bff-guide) uses the same `ApiSample` with a signed-in user instead of a service.
