Keycloak and Auth0 Setup

Run Keycloak Locally

Start Keycloak

Keycloak is a free, open-source authorization server. Run it with Docker on port 8180 so it does not clash with your Spring app:

docker run -d --name keycloak -p 8180:8080 \
  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
  -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
  quay.io/keycloak/keycloak:26.0 start-dev

Example output

Unable to find image 'quay.io/keycloak/keycloak:26.0' locally
26.0: Pulling from keycloak/keycloak
Status: Downloaded newer image for quay.io/keycloak/keycloak:26.0
3f6c2a9b8e1d4c7f0a5b2e8d9c1f4a7b6e3d0c9f8a2b5e1d4c7f0a3b6e9d2c5f

$ docker logs keycloak | tail -2
2026-03-01 04:30:12,118 INFO  [io.quarkus] (main) Keycloak 26.0.7 on JVM (powered by Quarkus 3.15.1) started in 9.412s. Listening on: http://0.0.0.0:8080
2026-03-01 04:30:12,119 INFO  [io.quarkus] (main) Profile dev activated.

Configure the Realm

  1. Open http://localhost:8180, log in as admin, and create a realm shopease.
  2. Create a client shopease-web: OpenID Connect, Client authentication OFF (public client), Standard flow ON, valid redirect URI http://localhost:5173/* for dev only. PKCE: Advanced → "Proof Key for Code Exchange Code Challenge Method" = S256.
  3. Create a client shopease-api (bearer-only use) and add client roles such as seller.
  4. Add Client scopes orders.read and orders.write, and an Audience mapper that adds shopease-api to the aud claim (otherwise aud is only account and the API rejects the token).
  5. Create realm roles customer and admin, and a test user asha with role customer.

Get a Test Token

Password grant (dev only)

For local testing only, enable "Direct access grants" on a separate shopease-cli client and get a token from the command line:

TOKEN=$(curl -s -X POST http://localhost:8180/realms/shopease/protocol/openid-connect/token \
  -d grant_type=password -d client_id=shopease-cli \
  -d username=asha -d password=asha-dev-pass -d scope="openid orders.read" | jq -r .access_token)

curl -s http://localhost:8081/api/orders -H "Authorization: Bearer $TOKEN"

Example output

[{"id":1042,"status":"SHIPPED","total":2499.00,"placedAt":"2026-02-27T18:22:05"},
 {"id":1057,"status":"PLACED","total":899.00,"placedAt":"2026-03-01T09:41:30"}]

Using Auth0 Instead

SettingKeycloakAuth0
issuer-urihttp://localhost:8180/realms/shopeasehttps://shopease.us.auth0.com/ (note the trailing slash)
AudienceAudience mapper on a client scopeCreate an API with identifier https://api.shopease.in; clients must request audience=...
Rolesrealm_access.roles, resource_access.*.rolesEnable RBAC → permissions claim, or add roles with an Action
CostFree, you run itManaged; free tier, then paid

application.yml for Auth0

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://shopease.us.auth0.com/
          audiences: https://api.shopease.in

Before Spring Boot added the audiences property, you wrote the audience check yourself. It is still useful when you need custom rules:

@Bean
JwtDecoder jwtDecoder(@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") String issuer) {
    NimbusJwtDecoder decoder = JwtDecoders.fromIssuerLocation(issuer);
    OAuth2TokenValidator<Jwt> audience = new JwtClaimValidator<List<String>>(
            JwtClaimNames.AUD, aud -> aud != null && aud.contains("https://api.shopease.in"));
    decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
            JwtValidators.createDefaultWithIssuer(issuer), audience));
    return decoder;
}