Real World Example

Where You See This

  • Fintech and banking apps often run their own authorization server because regulators require user data to stay in-house.
  • Multi-brand retailers (one login across grocery, fashion and electronics apps) run a central identity service — "one account for all our apps".
  • SaaS products offering "Login with OurProduct" to partners build an OAuth2 provider much like this.

ShopEase Identity: auth.shopease.in

ClientTypeGrantsScopes
shopease-web (React)public, PKCEauthorization_code, refresh_tokenopenid profile orders.read orders.write
shopease-androidpublic, PKCEauthorization_code, refresh_tokenopenid profile orders.read orders.write
seller-portalconfidential (BFF)authorization_codeopenid seller.catalog seller.orders
order-serviceconfidentialclient_credentialspayments.write inventory.reserve
nightly-reportsconfidentialclient_credentialsreports.read

Signing keys are loaded from a keystore stored in a secret manager and rotated every 90 days. During rotation the JWKS publishes both keys so tokens signed with the old key stay valid until they expire:

@Bean
JWKSource<SecurityContext> jwkSource(KeyStoreKeyLoader loader) {
    RSAKey current = loader.load("shopease-2026-03");   // signs new tokens (first key is used)
    RSAKey previous = loader.load("shopease-2025-12");  // still published for verification
    return new ImmutableJWKSet<>(new JWKSet(List.of(current, previous)));
}

JWKS during rotation

Resource servers pick the right key using the kid header:

curl -s http://auth.shopease.in/oauth2/jwks | jq ".keys[] | {kid, kty, alg: .alg}"

Example output

{ "kid": "shopease-2026-03", "kty": "RSA", "alg": null }
{ "kid": "shopease-2025-12", "kty": "RSA", "alg": null }