Azure 32: Entra ID + Spring Boot — Resource Server
easy⏱ 5 mincourseazure
JWT anatomy
A JWT has three base64url parts joined with dots: header.payload.signature. The header names the algorithm (RS256) and the signing key ID (kid). The payload carries claims — iss (issuer), aud (audience), exp (expiry), sub (user), and scp or roles (permissions). The signature is RS256(header + '.' + payload, privateKey) — the resource server verifies it against Entra ID's public JWKS.
// Example payload (decoded)
{
"iss": "https://login.microsoftonline.com/{tenantId}/v2.0",
"aud": "api://orders-api",
"exp": 1735689600,
"scp": "orders.read orders.write",
"sub": "alice@contoso.com"
}
Spring Boot + Azure AD starter
Adding spring-cloud-azure-starter-active-directory and configuring spring.cloud.azure.active-directory.credential.client-id + spring.cloud.azure.active-directory.profile.tenant-id wires up a resource server. Spring Security auto-validates every incoming Authorization: Bearer ... token against Entra ID's JWKS, rejects expired/untrusted tokens, and exposes scopes as SCOPE_orders.read authorities.
# application.yml
spring:
cloud:
azure:
active-directory:
profile:
tenant-id: ${AZURE_TENANT_ID}
credential:
client-id: ${AZURE_CLIENT_ID}
app-id-uri: api://orders-api
Scope-based authorization
Use @PreAuthorize("hasAuthority('SCOPE_orders.read')") on controllers or services. Spring matches the authority against the scp claim in the token. For application permissions (daemon-to-API), use ROLE_ prefix instead — the roles claim maps to ROLE_... authorities.
@RestController
@RequestMapping("/orders")
class OrderController {
@GetMapping
@PreAuthorize("hasAuthority('SCOPE_orders.read')")
fun list(): List<Order> = service.findAll()
@PostMapping
@PreAuthorize("hasAuthority('SCOPE_orders.write')")
fun create(@RequestBody o: Order): Order = service.save(o)
}
Build a JWT validator
Create a JwtValidator configured with expectedIssuer and expectedAudience. Implement validate(token) that splits the JWT, base64-decodes the payload, and checks iss, aud, and exp — returning { valid, reason }. Implement hasScope(token, scope) that returns whether the scope appears in the scp claim.
Use Managed Identity in AKS
Don't put client secrets in Helm values or pipeline variables. Enable Workload Identity on your AKS cluster, bind the pod's service account to a federated Entra ID credential, and the Spring Boot app gets tokens via the instance metadata endpoint — zero secrets, zero rotation.
Quiz: Why validate the audience claim?
Why is checking aud critical? Token confusion attacks. Without the audience check, a token issued for a different API in the same tenant could be accepted by yours. aud must match your API's App ID URI — otherwise reject the request with 401.