OAuth2/OIDC Authentication (Work in Progress)
|
This authentication method is currently under development and represents future functionality. The design documented here is subject to change as implementation progresses. |
Overview
The OAuth2/OIDC authentication flow provides an alternative authentication method for organizations that use external Identity Providers (IdP) with Single Sign-On (SSO) capabilities. This authentication path coexists with the hash-based JWT authentication used for simpler integrations like WordPress sites.
After successful OAuth2 authentication with an external IdP, the system exchanges the IdP’s JWT token for an internal JWT token that contains the claims necessary for multi-dimensional authorization.
When to Use OAuth2 vs Hash-Based Auth
| Authentication Method | Use Case | Example Scenarios |
|---|---|---|
OAuth2/OIDC |
Organizations with existing SSO infrastructure |
Corporate identity providers, Keycloak, Azure AD, Okta, Auth0 |
Hash-Based JWT |
Websites without OAuth/OIDC capability |
WordPress sites with custom plugin, legacy CMS platforms |
Authentication Flow
Step 1: OAuth2 Authorization Code Flow
The Registration Portal acts as an OAuth2 client using JHipster’s default OAuth2 implementation for gateways.
# application.yml (Registration Portal Gateway)
spring:
security:
oauth2:
client:
registration:
# Configured per tenant/organization
tenant-idp:
client-id: ${OAUTH2_CLIENT_ID}
client-secret: ${OAUTH2_CLIENT_SECRET}
scope: openid, profile, email
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
provider:
tenant-idp:
issuer-uri: ${OAUTH2_ISSUER_URI}
user-name-attribute: sub
User Flow:
-
User clicks "Login with SSO" button
-
Gateway redirects to external IdP login page
-
User authenticates with IdP credentials
-
IdP redirects back to gateway with authorization code
-
Gateway exchanges code for IdP JWT token (access token + ID token)
Step 2: Token Exchange with Admin Service
Once the gateway receives the IdP JWT, it must exchange this token for an internal JWT that contains:
-
OrgUser ID
-
Organization ID
-
Person ID (if linked)
-
Security permissions for multi-dimensional authorization
Step 3: OrgUser Lookup and Mapping
The Admin Service resolves the IdP user identity to an internal OrgUser entity:
@Entity
@Table(name = "org_user")
public class OrgUser {
@Id
private Long id;
@ManyToOne
@JoinColumn(name = "org_id")
private Organisation organisation;
// Maps to IdP 'sub' claim
@Column(name = "external_user_id")
private String externalUserId;
@Column(name = "email")
private String email;
@Column(name = "display_name")
private String displayName;
@ManyToOne
@JoinColumn(name = "person_id")
private Person person; // Optional: linked person for authorization
// ... other fields
}
Mapping Strategy:
-- Lookup query: Find OrgUser by RegistrationSystem + IdP subject
SELECT ou.*
FROM org_user ou
JOIN organisation org ON org.id = ou.org_id
JOIN registration_system rs ON rs.org_id = org.id
WHERE rs.id = :registrationSystemId
AND ou.external_user_id = :idpSubject;
Key Points:
-
RegistrationSystemIdidentifies which tenant/organization in Registration Portal -
Each Registration Portal tenant is linked to exactly one
RegistrationSystemin Admin Service -
RegistrationSystemis linked to exactly oneOrganisation -
OrgUser.externalUserIdmatches the IdPsubclaim -
If
OrgUser.personis set, this user can access person-scoped data
Step 4: Internal JWT Generation
The Admin Service generates an internal JWT with claims needed for multi-dimensional authorization:
{
"sub": "orguser:12345",
"iss": "admin-service",
"exp": 1735689600,
"orgUserId": 12345,
"orgId": 100,
"personId": 5678,
"authorities": ["ROLE_USER"],
"email": "[email protected]",
"displayName": "John Doe",
"externalSub": "idp-sub-claim-value"
}
Step 5: Session-Based JWT Storage
Like the hash-based authentication flow, the internal JWT is stored in the HTTP session and never exposed to the Angular frontend:
// Gateway: Store JWT in session after token exchange
@PostMapping("/api/auth/oauth2/token-exchange")
public ResponseEntity<Void> exchangeOAuth2Token(
@RequestHeader("Authorization") String idpToken,
@RequestParam("registrationSystemId") Long registrationSystemId,
HttpSession session) {
// Exchange IdP JWT with Admin Service
TokenResponse tokenResponse = adminServiceClient.exchangeOAuth2Token(
idpToken,
registrationSystemId
);
// Store internal JWT in HTTP session (NOT sent to client)
session.setAttribute("JWT_TOKEN", tokenResponse.getToken());
session.setAttribute("JWT_EXPIRES_AT", tokenResponse.getExpiresAt());
session.setAttribute("ORG_USER_ID", tokenResponse.getOrgUserId());
return ResponseEntity.ok().build();
}
Implementation Details
Gateway Configuration
The Registration Portal gateway uses JwtIssuerAuthenticationManagerResolver to accept JWTs from multiple issuers (external IdPs):
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.successHandler(new OAuth2AuthenticationSuccessHandler())
)
.oauth2ResourceServer(oauth2 -> oauth2
.authenticationManagerResolver(
new JwtIssuerAuthenticationManagerResolver(
"https://idp1.example.com",
"https://idp2.example.com"
)
)
);
return http.build();
}
}
OAuth2 Success Handler
After successful OAuth2 authentication, trigger token exchange:
@Component
public class OAuth2AuthenticationSuccessHandler
extends SimpleUrlAuthenticationSuccessHandler {
@Autowired
private AdminServiceClient adminServiceClient;
@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
OAuth2AuthenticationToken oauth2Token =
(OAuth2AuthenticationToken) authentication;
// Extract IdP JWT token
OAuth2AuthorizedClient authorizedClient =
authorizedClientService.loadAuthorizedClient(
oauth2Token.getAuthorizedClientRegistrationId(),
oauth2Token.getName()
);
String idpAccessToken = authorizedClient
.getAccessToken()
.getTokenValue();
// Determine RegistrationSystemId from tenant context
Long registrationSystemId = tenantContext.getRegistrationSystemId();
// Exchange IdP JWT for internal JWT
TokenResponse tokenResponse = adminServiceClient.exchangeOAuth2Token(
"Bearer " + idpAccessToken,
registrationSystemId
);
// Store internal JWT in session
HttpSession session = request.getSession();
session.setAttribute("JWT_TOKEN", tokenResponse.getToken());
session.setAttribute("JWT_EXPIRES_AT", tokenResponse.getExpiresAt());
// Redirect to application
super.onAuthenticationSuccess(request, response, authentication);
}
}
Admin Service Token Exchange Endpoint
@RestController
@RequestMapping("/api/auth")
public class TokenExchangeController {
@Autowired
private JwtDecoder jwtDecoder; // Validates IdP JWT
@Autowired
private OrgUserService orgUserService;
@Autowired
private JwtTokenProvider tokenProvider;
@PostMapping("/token-exchange/oauth2")
public ResponseEntity<TokenResponse> exchangeOAuth2Token(
@RequestHeader("Authorization") String idpToken,
@RequestParam("registrationSystemId") Long registrationSystemId) {
// 1. Validate IdP JWT signature using JwtIssuerAuthenticationManagerResolver
Jwt jwt = jwtDecoder.decode(idpToken.replace("Bearer ", ""));
// 2. Extract IdP claims
String idpSubject = jwt.getClaimAsString("sub");
String email = jwt.getClaimAsString("email");
String displayName = jwt.getClaimAsString("name");
// 3. Lookup OrgUser by RegistrationSystemId + IdP subject
OrgUser orgUser = orgUserService.findByRegistrationSystemAndExternalUserId(
registrationSystemId,
idpSubject
).orElseThrow(() -> new UnauthorizedException(
"No OrgUser found for IdP subject: " + idpSubject
));
// 4. Generate internal JWT with OrgUser claims
String internalJwt = tokenProvider.createToken(
orgUser.getId(),
orgUser.getOrganisation().getId(),
orgUser.getPerson() != null ? orgUser.getPerson().getId() : null,
email,
displayName
);
// 5. Return internal JWT
return ResponseEntity.ok(TokenResponse.builder()
.token(internalJwt)
.expiresAt(Instant.now().plus(12, ChronoUnit.HOURS))
.orgUserId(orgUser.getId())
.build());
}
}
OrgUser Service Lookup
@Service
public class OrgUserService {
@Autowired
private OrgUserRepository orgUserRepository;
/**
* Find OrgUser by RegistrationSystemId and IdP subject claim.
*
* This resolves:
* RegistrationSystem -> Organisation -> OrgUser (by externalUserId)
*/
public Optional<OrgUser> findByRegistrationSystemAndExternalUserId(
Long registrationSystemId,
String externalUserId) {
return orgUserRepository.findByRegistrationSystemIdAndExternalUserId(
registrationSystemId,
externalUserId
);
}
}
@Repository
public interface OrgUserRepository extends JpaRepository<OrgUser, Long> {
@Query("""
SELECT ou FROM OrgUser ou
JOIN ou.organisation org
JOIN org.registrationSystems rs
WHERE rs.id = :registrationSystemId
AND ou.externalUserId = :externalUserId
""")
Optional<OrgUser> findByRegistrationSystemIdAndExternalUserId(
@Param("registrationSystemId") Long registrationSystemId,
@Param("externalUserId") String externalUserId
);
}
JWT Validation Configuration
The Admin Service must validate JWTs from multiple issuers (external IdPs):
@Configuration
public class JwtConfiguration {
@Bean
public JwtDecoder jwtDecoder() {
// Support multiple IdP issuers
return new JwtIssuerAuthenticationManagerResolver(
"https://idp1.example.com",
"https://idp2.example.com"
).jwtDecoder();
}
}
# application.yml (Admin Service)
spring:
security:
oauth2:
resourceserver:
jwt:
# Support multiple issuers for token validation
issuer-uri: ${JWT_ISSUER_URI:https://idp.example.com}
audiences:
- admin-service
- registration-portal
Tenant Configuration
Each tenant in the Registration Portal can be configured with OAuth2/OIDC settings:
{
"tenantId": "acme-corp",
"registrationSystemId": 100,
"authenticationMethod": "OAUTH2",
"oauth2Config": {
"clientId": "registration-portal",
"clientSecret": "${OAUTH2_CLIENT_SECRET}",
"issuerUri": "https://keycloak.acme.com/auth/realms/acme",
"scopes": ["openid", "profile", "email"],
"userNameAttribute": "sub"
}
}
Error Scenarios
| Error | Cause | Resolution |
|---|---|---|
OrgUser Not Found |
No OrgUser with matching |
Create OrgUser in Admin Service with IdP |
Invalid IdP JWT |
IdP JWT signature validation fails |
Check IdP configuration, ensure correct issuer URI and JWKS endpoint |
Expired IdP JWT |
IdP access token expired |
Implement token refresh flow using refresh token |
Multiple IdP Issuers |
Admin Service cannot determine which IdP issued the JWT |
Use |
RegistrationSystem Not Found |
Invalid |
Verify tenant configuration in Registration Portal |
Security Considerations
|
Critical Security Requirements:
|
Integration with Multi-Dimensional Security
After successful OAuth2 authentication and token exchange, the internal JWT contains all claims necessary for multi-dimensional authorization:
-
Organizational Access: Determined by
OrgUser.orgId -
Personal Access: Determined by
OrgUser.personId(if linked) -
Permissions: Resolved through
OrganisationUserandPersonLinkrelationships
See JWT Authentication for details on how the internal JWT is used for authorization.
Future Enhancements
|
Planned Features:
|
Related Documentation
-
JWT Authentication - Hash-based authentication flow
-
Security Dimensions - Multi-dimensional authorization
-
Security Entities - OrgUser entity definition
-
Registration Portal Security - Gateway implementation
OAuth2/OIDC authentication is under active development. This documentation will be updated as implementation progresses.