Skip to content

feat: MS Entra Bearer token authentication (#127)#158

Merged
oto-macenauer-absa merged 9 commits intomasterfrom
feature/127-ms-entra-authentication
Apr 9, 2026
Merged

feat: MS Entra Bearer token authentication (#127)#158
oto-macenauer-absa merged 9 commits intomasterfrom
feature/127-ms-entra-authentication

Conversation

@oto-macenauer-absa
Copy link
Copy Markdown
Collaborator

@oto-macenauer-absa oto-macenauer-absa commented Mar 11, 2026

Summary

Adds MS Entra (Azure AD) as a new login mechanism. Users holding a valid Entra Bearer JWT can now exchange it for a login-service access+refresh token pair alongside existing Basic Auth, LDAP and Kerberos methods.

Changes

  • MsEntraConfig — PureConfig case class (tenant-id, client-id, audience, order, optional attributes map); implements ConfigValidatable
  • MsEntraTokenValidator — validates Entra JWTs via OIDC discovery + Nimbus JOSE; JWKS cached 1 h via Guava LoadingCache; injectable JWKSource override for tests (no HTTP calls)
  • MsEntraBearerTokenFilter — Spring OncePerRequestFilter; intercepts Authorization: Bearer headers; populates SecurityContext on success, returns HTTP 401 on failure; skips when context already populated
  • SecurityConfig — conditionally registers the filter before BasicAuthenticationFilter when entra.order > 0
  • AuthConfigProvider / ConfigProvidergetMsEntraConfig plumbing
  • Dependencies — Guava CacheBuilder added to apiDependencies
  • example.application.yaml — commented entra: config block added

Tests

Three new test files, all 153 tests pass:

  • MsEntraConfigTest — validation logic
  • MsEntraTokenValidatorTest — real RSA key pair, injected ImmutableJWKSet, no HTTP
  • MsEntraBearerTokenFilterTest — Mockito mock validator, MockHttpServletRequest/Response

Release notes:

  • Add MS Entra login mechanism

Configuration

To enable, add to application.yaml:

loginsvc.rest.auth.provider:
  entra:
    order: 3
    tenant-id: "<tenant-id>"
    client-id: "<client-id>"
    audience: "api://<client-id>"

Closes #127

Allow users holding a valid MS Entra (Azure AD) JWT to exchange it for
a login-service access+refresh token pair via the existing /token/generate
endpoint — alongside Basic Auth, LDAP, and Kerberos.

Implementation details:
- MsEntraConfig: PureConfig case class with tenant-id, client-id, audience,
  order and optional attributes map; implements ConfigValidatable
- MsEntraTokenValidator: validates Entra JWTs via OIDC discovery + Nimbus
  JOSE; JWKS cached 1h via Guava LoadingCache; injectable JWKSource for tests
- MsEntraBearerTokenFilter: OncePerRequestFilter; intercepts Authorization:
  Bearer headers; populates SecurityContext on success; returns 401 on failure
- SecurityConfig: conditionally registers the filter before BasicAuthFilter
  when entra.order > 0
- AuthConfigProvider/ConfigProvider: getMsEntraConfig plumbing
- Dependencies: add Guava CacheBuilder to apiDependencies
- example.application.yaml: commented entra config block
- Tests: MsEntraConfigTest, MsEntraTokenValidatorTest (real RSA key pair,
  no HTTP), MsEntraBearerTokenFilterTest (Mockito); all 153 tests pass
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 11, 2026

Report: Report: api - scala:2.12.17

Metric (instruction) Coverage Threshold Status
Overall 71.57% 43.0%
Changed Files 71.49% 70.0%
File Path Coverage Threshold Status
AuthConfigProvider.scala 0.0% 0.0%
ConfigProvider.scala 78.03% 0.0%
MsEntraBearerTokenFilter.scala 96.61% 0.0%
MsEntraConfig.scala 74.65% 0.0%
MsEntraGraphClient.scala 97.24% 0.0%
MsEntraTokenValidator.scala 72.1% 0.0%
SecurityConfig.scala 81.82% 0.0%

oto-macenauer-absa and others added 5 commits March 11, 2026 10:58
- Replace single 'audience: String' with 'audiences: List[String]' in MsEntraConfig
- Empty list means accept any audience from the tenant (only issuer + signature verified)
- Non-empty list requires token aud to intersect with configured audiences
- Audience check is done manually (post-processing) to support 'any of' semantics,
  since Nimbus DefaultJWTClaimsVerifier uses 'all of' semantics
- Add run/fork and run/baseDirectory to build.sbt to fix sbt run with TomcatPlugin
- Add run/javaOptions for macOS Keychain trust store (corporate proxy SSL)
- Add MsEntraGraphClient: calls Graph API with client credentials to
  look up onPremisesSamAccountName and onPremisesDomainName for the
  authenticated user, returning NETBIOS\samAccountName format
- Add GraphUsernameResolver trait for testability
- Add clientSecret and domains fields to MsEntraConfig (both optional;
  when absent, username resolution falls back to UPN from the token)
- MsEntraTokenValidator: use graph resolver when clientSecret is
  configured; fall back to preferred_username/upn/sub when None
- Add two tests: graph resolves username, graph returns None → UPN fallback
- Update example.application.yaml with new config fields documentation
- Add User.Read.All application permission to Login Service - DEV app

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Return lowercase samAccountName values from Graph-backed Entra resolution, keep domain mappings as the allow-list for recognized domains, and update logging/tests/docs to reflect the new behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@oto-macenauer-absa
Copy link
Copy Markdown
Collaborator Author

Updated this PR to normalize Graph-resolved Entra usernames to lowercase samAccountName values without the domain prefix.

What changed:

  • Graph-backed Entra resolution now returns ab006hm instead of CORP\AB006HM
  • configured domains entries remain the allow-list for recognized onPremisesDomainName values
  • successful resolution still logs the mapped AB/NetBIOS value for traceability
  • tests/docs were updated to match the new behavior

What this allows:

  • users from allowed on-prem domains can authenticate with the username format expected by the local environment
  • known Graph domains are still validated before accepting the resolved username
  • troubleshooting keeps the domain-to-AB mapping visible in logs without putting the prefix into the token username

Copy link
Copy Markdown
Collaborator

@dk1844 dk1844 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM so far

I would like to test it with our LS dev with our MS Entra AppId + Object ID

oto-macenauer-absa and others added 2 commits March 16, 2026 13:23
Add focused Graph client tests for username normalization and error branches, and make endpoint URLs injectable for realistic local HTTP coverage tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace real-looking sample identifiers in MsEntraGraphClientTest with made-up usernames and email UPNs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment on lines +69 to +71
tokenEndpointOverride.getOrElse(s"https://login.microsoftonline.com/${config.tenantId}/oauth2/v2.0/token")

private val graphUsersBaseUrl = "https://graph.microsoft.com/v1.0/users"
private val graphUsersBaseUrl = graphUsersBaseUrlOverride.getOrElse("https://graph.microsoft.com/v1.0/users")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I strongly believe that these should not be in code. I know these are fallbacks but even so.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 3d1de53. All hardcoded Microsoft endpoint URLs have been moved into MsEntraConfig as two optional fields with public-cloud defaults:

  • loginBaseUrl (default: https://login.microsoftonline.com)
  • graphBaseUrl (default: https://graph.microsoft.com)

All derived URLs (token endpoint, Graph users path, Graph scope, OIDC discovery URL, expected JWT issuer) are now computed from these config values. The test constructor overrides (tokenEndpointOverride/graphUsersBaseUrlOverride) have been removed — tests now pass custom base URLs via MsEntraConfig directly. The example.application.yaml documents both fields.

Hardcoded Microsoft endpoint URLs have been removed from code.
Two new optional fields on MsEntraConfig with public-cloud defaults:

  loginBaseUrl (default: https://login.microsoftonline.com)
  graphBaseUrl  (default: https://graph.microsoft.com)

All derived URLs (token endpoint, Graph users path, Graph scope,
OIDC discovery URL, expected JWT issuer) are now computed from
these config values.  Sovereign-cloud deployments (e.g. Azure
Government) can override them without touching code.

MsEntraGraphClient constructor overrides (tokenEndpointOverride,
graphUsersBaseUrlOverride) are removed; tests now pass custom base
URLs via MsEntraConfig directly.

Addresses review comment on PR #158.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Collaborator

@dk1844 dk1844 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (read the code). Looking forward to start experimenting with it.

@oto-macenauer-absa oto-macenauer-absa merged commit 767e41f into master Apr 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MS Entra integraton

2 participants