Skip to content

Commit

Permalink
Allow token verification with user info when no introspection endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Dec 6, 2022
1 parent c8bd1ba commit a292ef7
Show file tree
Hide file tree
Showing 18 changed files with 211 additions and 41 deletions.
9 changes: 9 additions & 0 deletions docs/src/main/asciidoc/security-openid-connect-providers.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,13 @@ Now add the following configuration to your `application.properties`:
[source,properties]
----
quarkus.oidc.provider=github
quarkus.oidc.application-type=web-app
quarkus.oidc.client-id=<Client ID>
quarkus.oidc.credentials.secret=<Secret>
----

TIP: You can also use GitHub provider with `quarkus.oidc.application-type=service`, just set configuration property `quarkus.oidc.allow-bearer-token-verification-with-user-info=true` to true.

=== Google

In order to set up OIDC for Google you need to create a new project in your https://console.cloud.google.com/projectcreate[Google Cloud Platform console]:
Expand Down Expand Up @@ -95,6 +98,7 @@ You can now configure your `application.properties`:
[source,properties]
----
quarkus.oidc.provider=google
quarkus.oidc.application-type=web-app
quarkus.oidc.client-id=<Client ID>
quarkus.oidc.credentials.secret=<Secret>
----
Expand Down Expand Up @@ -136,6 +140,7 @@ You can now configure your `application.properties`:
[source,properties]
----
quarkus.oidc.provider=microsoft
quarkus.oidc.application-type=web-app
quarkus.oidc.client-id=<Client ID>
quarkus.oidc.credentials.secret=<Secret>
----
Expand Down Expand Up @@ -239,6 +244,7 @@ You can now configure your `application.properties`:
[source,properties]
----
quarkus.oidc.provider=apple
quarkus.oidc.application-type=web-app
quarkus.oidc.client-id=<Bundle ID>
quarkus.oidc.credentials.jwt.key-file=AuthKey_<Key ID>.p8
quarkus.oidc.credentials.jwt.token-key-id=<Key ID>
Expand Down Expand Up @@ -281,6 +287,7 @@ You can now configure your `application.properties`:
[source,properties]
----
quarkus.oidc.provider=facebook
quarkus.oidc.application-type=web-app
quarkus.oidc.client-id=<App ID>
quarkus.oidc.credentials.secret=<App secret>
----
Expand Down Expand Up @@ -337,6 +344,7 @@ You can now configure your `application.properties`:
[source,properties]
----
quarkus.oidc.provider=twitter
quarkus.oidc.application-type=web-app
quarkus.oidc.client-id=<Client ID>
quarkus.oidc.credentials.secret=<Client Secret>
----
Expand All @@ -356,6 +364,7 @@ You can now configure your `application.properties`:
[source,properties]
----
quarkus.oidc.provider=spotify
quarkus.oidc.application-type=web-app
quarkus.oidc.client-id=<Client ID>
quarkus.oidc.credentials.secret=<Client Secret>
----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,7 @@ Here is how you can integrate `quarkus-oidc` with GitHub after you have link:htt
[source,properties]
----
quarkus.oidc.provider=github
quarkus.oidc.application-type=web-app
quarkus.oidc.client-id=github_app_clientid
quarkus.oidc.credentials.secret=github_app_clientsecret
Expand Down Expand Up @@ -923,6 +924,7 @@ and configure Google OIDC properties:
[source, properties]
----
quarkus.oidc.provider=google
quarkus.oidc.application-type=web-app
quarkus.oidc.client-id={GOOGLE_CLIENT_ID}
quarkus.oidc.credentials.secret={GOOGLE_CLIENT_SECRET}
quarkus.oidc.token.issuer=https://accounts.google.com
Expand Down Expand Up @@ -1106,6 +1108,7 @@ Apple OpenID Connect Provider uses a `client_secret_post` method where a secret
----
# Apple provider configuration sets a 'client_secret_post_jwt' authentication method
quarkus.oidc.provider=apple
quarkus.oidc.application-type=web-app
quarkus.oidc.client-id=${apple.client-id}
quarkus.oidc.credentials.jwt.key-file=ecPrivateKey.pem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,15 @@ public static enum ApplicationType {
@ConfigItem
public Optional<Provider> provider = Optional.empty();

/**
* Indirectly verify the user authentication has been successful with request to `user-info-path`.
* Bearer token is considered valid if `user-info-path` endpoint responded with `200 OK`.
* You should only enable this option with OIDC non-compliant provider without introspection endpoint.
* Please make sure 'user-info' endpoint is secured.
*/
@ConfigItem(defaultValue = "false")
public boolean allowBearerTokenVerificationWithUserInfo;

public static enum Provider {
APPLE,
FACEBOOK,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public Uni<SecurityIdentity> apply(UserInfo userInfo, Throwable t) {

private Uni<SecurityIdentity> createSecurityIdentityWithOidcServer(RoutingContext vertxContext,
TokenAuthenticationRequest request, TenantConfigContext resolvedContext, final UserInfo userInfo) {
Uni<TokenVerificationResult> tokenUni = null;
final Uni<TokenVerificationResult> tokenUni;
if (isInternalIdToken(request)) {
if (vertxContext.get(NEW_AUTHENTICATION) == Boolean.TRUE) {
// No need to verify it in this case as 'CodeAuthenticationMechanism' has just created it
Expand All @@ -148,7 +148,17 @@ private Uni<SecurityIdentity> createSecurityIdentityWithOidcServer(RoutingContex
tokenUni = verifySelfSignedTokenUni(resolvedContext, request.getToken().getToken());
}
} else {
tokenUni = verifyTokenUni(resolvedContext, request.getToken().getToken());
if (resolvedContext.provider.verifyUserAuthSuccessWithUserInfo) {
if (userInfo == null) {
tokenUni = Uni.createFrom().failure(
new AuthenticationFailedException("Bearer token verification failed as user info is null."));
} else {
// valid token verification result with empty JWT content
tokenUni = Uni.createFrom().item(new TokenVerificationResult(new JsonObject(), null));
}
} else {
tokenUni = verifyTokenUni(resolvedContext, request.getToken().getToken());
}
}

return tokenUni.onItemOrFailure()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public class OidcProvider implements Closeable {
final String[] audience;
final Map<String, String> requiredClaims;
final Key tokenDecryptionKey;
final boolean verifyUserAuthSuccessWithUserInfo;

public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, JsonWebKeySet jwks, Key tokenDecryptionKey) {
this.client = client;
Expand All @@ -70,6 +71,7 @@ public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, Json
this.audience = checkAudienceProp();
this.requiredClaims = checkRequiredClaimsProp();
this.tokenDecryptionKey = tokenDecryptionKey;
this.verifyUserAuthSuccessWithUserInfo = oidcConfig != null && oidcConfig.allowBearerTokenVerificationWithUserInfo;
}

public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenDecryptionKey) {
Expand All @@ -80,6 +82,7 @@ public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenD
this.audience = checkAudienceProp();
this.requiredClaims = checkRequiredClaimsProp();
this.tokenDecryptionKey = tokenDecryptionKey;
this.verifyUserAuthSuccessWithUserInfo = oidcConfig != null && oidcConfig.allowBearerTokenVerificationWithUserInfo;
}

private String checkIssuerProp() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,23 @@ private Uni<TenantConfigContext> createTenantContext(Vertx vertx, OidcTenantConf
}
}

if (oidcConfig.allowBearerTokenVerificationWithUserInfo) {
if (!oidcConfig.authentication.isUserInfoRequired().orElse(false)) {
throw new ConfigurationException(
"UserInfo is not required but 'allowBearerTokenVerificationWithUserInfo' is enabled");
}
if (!oidcConfig.isDiscoveryEnabled().orElse(true)) {
if (oidcConfig.userInfoPath.isEmpty()) {
throw new ConfigurationException(
"UserInfo path is missing but 'allowBearerTokenVerificationWithUserInfo' is enabled");
}
if (oidcConfig.introspectionPath.isPresent()) {
throw new ConfigurationException(
"Introspection path is configured and 'allowBearerTokenVerificationWithUserInfo' is enabled, these options are mutually exclusive");
}
}
}

return createOidcProvider(oidcConfig, tlsConfig, vertx)
.onItem().transform(p -> new TenantConfigContext(p, oidcConfig));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,8 @@ static OidcTenantConfig mergeTenantConfig(OidcTenantConfig tenant, OidcTenantCon
static OidcTenantConfig resolveProviderConfig(OidcTenantConfig oidcTenantConfig) {
if (oidcTenantConfig != null && oidcTenantConfig.provider.isPresent()) {
return OidcUtils.mergeTenantConfig(oidcTenantConfig,
KnownOidcProviders.provider(oidcTenantConfig.provider.get()));
KnownOidcProviders.provider(oidcTenantConfig.provider.get(),
oidcTenantConfig.applicationType.orElse(OidcTenantConfig.ApplicationType.SERVICE)));
} else {
return oidcTenantConfig;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,30 @@

public class KnownOidcProviders {

public static OidcTenantConfig provider(OidcTenantConfig.Provider provider) {
public static OidcTenantConfig provider(OidcTenantConfig.Provider provider,
OidcTenantConfig.ApplicationType applicationType) {
if (OidcTenantConfig.Provider.GITHUB == provider) {
return github();
return github(applicationType);
} else if (OidcTenantConfig.Provider.GOOGLE == provider) {
return google();
return google(applicationType);
} else if (OidcTenantConfig.Provider.APPLE == provider) {
return apple();
return apple(applicationType);
} else if (OidcTenantConfig.Provider.MICROSOFT == provider) {
return microsoft();
return microsoft(applicationType);
} else if (OidcTenantConfig.Provider.FACEBOOK == provider) {
return facebook();
return facebook(applicationType);
} else if (OidcTenantConfig.Provider.SPOTIFY == provider) {
return spotify();
return spotify(applicationType);
} else if (OidcTenantConfig.Provider.TWITTER == provider) {
return twitter();
return twitter(applicationType);
}
return null;
}

private static OidcTenantConfig github() {
private static OidcTenantConfig github(OidcTenantConfig.ApplicationType applicationType) {
OidcTenantConfig ret = new OidcTenantConfig();
ret.setAuthServerUrl("https://github.com/login/oauth");
ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP);
ret.setApplicationType(applicationType);
ret.setDiscoveryEnabled(false);
ret.setAuthorizationPath("authorize");
ret.setTokenPath("access_token");
Expand All @@ -42,10 +43,10 @@ private static OidcTenantConfig github() {
return ret;
}

private static OidcTenantConfig twitter() {
private static OidcTenantConfig twitter(OidcTenantConfig.ApplicationType applicationType) {
OidcTenantConfig ret = new OidcTenantConfig();
ret.setAuthServerUrl("https://api.twitter.com/2/oauth2");
ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP);
ret.setApplicationType(applicationType);
ret.setDiscoveryEnabled(false);
ret.setAuthorizationPath("https://twitter.com/i/oauth2/authorize");
ret.setTokenPath("token");
Expand All @@ -58,27 +59,27 @@ private static OidcTenantConfig twitter() {
return ret;
}

private static OidcTenantConfig google() {
private static OidcTenantConfig google(OidcTenantConfig.ApplicationType applicationType) {
OidcTenantConfig ret = new OidcTenantConfig();
ret.setAuthServerUrl("https://accounts.google.com");
ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP);
ret.setApplicationType(applicationType);
ret.getAuthentication().setScopes(List.of("openid", "email", "profile"));
return ret;
}

private static OidcTenantConfig microsoft() {
private static OidcTenantConfig microsoft(OidcTenantConfig.ApplicationType applicationType) {
OidcTenantConfig ret = new OidcTenantConfig();
ret.setAuthServerUrl("https://login.microsoftonline.com/common/v2.0");
ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP);
ret.setApplicationType(applicationType);
ret.getToken().setIssuer("any");
ret.getAuthentication().setScopes(List.of("openid", "email", "profile"));
return ret;
}

private static OidcTenantConfig facebook() {
private static OidcTenantConfig facebook(OidcTenantConfig.ApplicationType applicationType) {
OidcTenantConfig ret = new OidcTenantConfig();
ret.setAuthServerUrl("https://www.facebook.com");
ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP);
ret.setApplicationType(applicationType);
ret.setDiscoveryEnabled(false);
ret.setAuthorizationPath("https://facebook.com/dialog/oauth/");
ret.setTokenPath("https://graph.facebook.com/v12.0/oauth/access_token");
Expand All @@ -88,10 +89,10 @@ private static OidcTenantConfig facebook() {
return ret;
}

private static OidcTenantConfig apple() {
private static OidcTenantConfig apple(OidcTenantConfig.ApplicationType applicationType) {
OidcTenantConfig ret = new OidcTenantConfig();
ret.setAuthServerUrl("https://appleid.apple.com/");
ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP);
ret.setApplicationType(applicationType);
ret.getAuthentication().setScopes(List.of("openid", "email", "name"));
ret.getAuthentication().setForceRedirectHttpsScheme(true);
ret.getAuthentication().setResponseMode(ResponseMode.FORM_POST);
Expand All @@ -101,12 +102,12 @@ private static OidcTenantConfig apple() {
return ret;
}

private static OidcTenantConfig spotify() {
private static OidcTenantConfig spotify(OidcTenantConfig.ApplicationType applicationType) {
// See https://developer.spotify.com/documentation/general/guides/authorization/code-flow/
OidcTenantConfig ret = new OidcTenantConfig();
ret.setDiscoveryEnabled(false);
ret.setAuthServerUrl("https://accounts.spotify.com");
ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP);
ret.setApplicationType(applicationType);
ret.setAuthorizationPath("authorize");
ret.setTokenPath("api/token");
ret.setUserInfoPath("https://api.spotify.com/v1/me");
Expand Down
Loading

0 comments on commit a292ef7

Please sign in to comment.