diff --git a/README.md b/README.md index fa81616..a8d6cb7 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,45 @@ In the above example configuration, the process for failure will work as follows 7. 3rd retry attempt occurs and fails 8. Authentication fails and exception is communicated to user. +### MS Entra (Azure AD) Authentication Provider +Validates incoming Microsoft Entra (Azure AD) Bearer JWTs against Microsoft's JWKS +(fetched from the OIDC discovery endpoint and cached in memory), then issues a +login-service JWT. + +Example config: +``` + entra: + order: 1 + tenant-id: "your-tenant-id" + client-id: "your-client-id" + client-secret: "your-client-secret" + audiences: + - "api://your-client-id" + - "other-app-client-id" + domains: + corp.example.com: "CORP" + login-base-url: "https://login.microsoftonline.com" + graph-base-url: "https://graph.microsoft.com" + attributes: + preferred_username: "upn" + email: "email" + jwks-connect-timeout-ms: 10000 + jwks-read-timeout-ms: 10000 +``` + +Available options: +- `order` — provider ordering; `0` disables, `1`+ enables. +- `tenant-id` — Azure AD tenant (directory) ID. +- `client-id` — App registration (client) ID. +- `client-secret` — Client secret used to call MS Graph API. +- `audiences` — List of accepted JWT `aud` values. Empty list accepts any token from the tenant. +- `domains` — Map of on-premises DNS domains to short names (e.g. `corp.example.com: "CORP"`). +- `login-base-url` — Microsoft login/token endpoint base URL. Optional, defaults to `https://login.microsoftonline.com`. +- `graph-base-url` — Microsoft Graph API base URL. Optional, defaults to `https://graph.microsoft.com`. +- `attributes` — Map of Entra JWT claim names to LS JWT claim names. +- `jwks-connect-timeout-ms` — TCP connect timeout (ms) when fetching Microsoft's JWKS. Optional, defaults to `10000`. +- `jwks-read-timeout-ms` — Socket read timeout (ms) when downloading the JWKS payload. Optional, defaults to `10000`. + ### ActiveDirectoryLDAPAuthenticationProvider Uses LDAP(s) to authenticate user in Active Directory and to fetch groups that this user belongs to. diff --git a/api/src/main/resources/example.application.yaml b/api/src/main/resources/example.application.yaml index b6f3aed..e1bc929 100644 --- a/api/src/main/resources/example.application.yaml +++ b/api/src/main/resources/example.application.yaml @@ -135,6 +135,12 @@ loginsvc: #attributes: #preferred_username: "upn" #email: "email" + # JWKS fetching timeouts (milliseconds) used when downloading Microsoft's + # JSON Web Key Set from the OIDC discovery endpoint. Defaults to 10000 each + # (10 seconds) if omitted. Tune for slow networks or when Microsoft's + # endpoint is under load. + #jwks-connect-timeout-ms: 10000 + #jwks-read-timeout-ms: 10000 experimental: # ability to enable experimental endpoints (default=false) diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/MsEntraConfig.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/MsEntraConfig.scala index 8c80f85..706b41e 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/MsEntraConfig.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/config/auth/MsEntraConfig.scala @@ -41,6 +41,8 @@ import za.co.absa.loginsvc.rest.config.validation.{ConfigValidatable, ConfigVali * @param graphBaseUrl Base URL for the Microsoft Graph API. * Defaults to the public Azure cloud (`https://graph.microsoft.com`). * Override for sovereign clouds (e.g. Azure Government). + * @param jwksConnectTimeoutMs Connect timeout (in ms) for fetching the JWKS from Microsoft. Defaults to 10000. + * @param jwksReadTimeoutMs Read timeout (in ms) for fetching the JWKS from Microsoft. Defaults to 10000. */ case class MsEntraConfig( tenantId: String, @@ -51,7 +53,9 @@ case class MsEntraConfig( order: Int, attributes: Option[Map[String, String]], loginBaseUrl: String = "https://login.microsoftonline.com", - graphBaseUrl: String = "https://graph.microsoft.com" + graphBaseUrl: String = "https://graph.microsoft.com", + jwksConnectTimeoutMs: Int = 10000, + jwksReadTimeoutMs: Int = 10000 ) extends ConfigValidatable with ConfigOrdering { def throwErrors(): Unit = diff --git a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/entra/MsEntraTokenValidator.scala b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/entra/MsEntraTokenValidator.scala index c983135..3a0761e 100644 --- a/api/src/main/scala/za/co/absa/loginsvc/rest/provider/entra/MsEntraTokenValidator.scala +++ b/api/src/main/scala/za/co/absa/loginsvc/rest/provider/entra/MsEntraTokenValidator.scala @@ -20,6 +20,7 @@ import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache} import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.jwk.source.{JWKSource, RemoteJWKSet} import com.nimbusds.jose.proc.{JWSVerificationKeySelector, SecurityContext => NimbusSecurityContext} +import com.nimbusds.jose.util.DefaultResourceRetriever import com.nimbusds.jwt.proc.{BadJWTException, DefaultJWTClaimsVerifier, DefaultJWTProcessor} import com.nimbusds.jwt.{JWTClaimsSet, SignedJWT} import org.slf4j.LoggerFactory @@ -68,8 +69,11 @@ class MsEntraTokenValidator( .expireAfterWrite(1, TimeUnit.HOURS) .build(new CacheLoader[String, JWKSource[NimbusSecurityContext]] { override def load(jwksUri: String): JWKSource[NimbusSecurityContext] = { - logger.info(s"Loading JWKS from $jwksUri") - new RemoteJWKSet[NimbusSecurityContext](new URL(jwksUri)) + logger.info(s"Loading JWKS from $jwksUri (connectTimeoutMs=${config.jwksConnectTimeoutMs}, readTimeoutMs=${config.jwksReadTimeoutMs})") + new RemoteJWKSet[NimbusSecurityContext]( + new URL(jwksUri), + new DefaultResourceRetriever(config.jwksConnectTimeoutMs, config.jwksReadTimeoutMs) + ) } })