Skip to content

Commit

Permalink
Support for OAuth2 Strava
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Dec 22, 2023
1 parent 77ab697 commit dc520ca
Show file tree
Hide file tree
Showing 11 changed files with 760 additions and 577 deletions.
Binary file added docs/src/main/asciidoc/images/oidc-strava-1.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 19 additions & 2 deletions docs/src/main/asciidoc/security-openid-connect-providers.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc
include::_attributes.adoc[]
:diataxis-type: concept
:categories: security,web
:keywords: oidc github twitter google facebook mastodon microsoft apple spotify twitch linkedin
:keywords: oidc github twitter google facebook mastodon microsoft apple spotify twitch linkedin strava

Check warning on line 11 in docs/src/main/asciidoc/security-openid-connect-providers.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Spelling] Use correct American English spelling. Did you really mean 'oidc'? Raw Output: {"message": "[Quarkus.Spelling] Use correct American English spelling. Did you really mean 'oidc'?", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-providers.adoc", "range": {"start": {"line": 11, "column": 12}}}, "severity": "WARNING"}
:toclevels: 3
:topics: security,oidc,github,twitter,google,facebook,mastodon,microsoft,apple,spotify,twitch
:topics: security,oidc,github,twitter,google,facebook,mastodon,microsoft,apple,spotify,twitch,linkedin,strava

Check warning on line 13 in docs/src/main/asciidoc/security-openid-connect-providers.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Spelling] Use correct American English spelling. Did you really mean 'facebook'? Raw Output: {"message": "[Quarkus.Spelling] Use correct American English spelling. Did you really mean 'facebook'?", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-providers.adoc", "range": {"start": {"line": 13, "column": 46}}}, "severity": "WARNING"}

Check warning on line 13 in docs/src/main/asciidoc/security-openid-connect-providers.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Spelling] Use correct American English spelling. Did you really mean 'microsoft'? Raw Output: {"message": "[Quarkus.Spelling] Use correct American English spelling. Did you really mean 'microsoft'?", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-providers.adoc", "range": {"start": {"line": 13, "column": 64}}}, "severity": "WARNING"}

Check warning on line 13 in docs/src/main/asciidoc/security-openid-connect-providers.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Spelling] Use correct American English spelling. Did you really mean 'spotify'? Raw Output: {"message": "[Quarkus.Spelling] Use correct American English spelling. Did you really mean 'spotify'?", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-providers.adoc", "range": {"start": {"line": 13, "column": 80}}}, "severity": "WARNING"}

Check warning on line 13 in docs/src/main/asciidoc/security-openid-connect-providers.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Spelling] Use correct American English spelling. Did you really mean 'linkedin'? Raw Output: {"message": "[Quarkus.Spelling] Use correct American English spelling. Did you really mean 'linkedin'?", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-providers.adoc", "range": {"start": {"line": 13, "column": 95}}}, "severity": "WARNING"}

Check warning on line 13 in docs/src/main/asciidoc/security-openid-connect-providers.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Spelling] Use correct American English spelling. Did you really mean 'strava'? Raw Output: {"message": "[Quarkus.Spelling] Use correct American English spelling. Did you really mean 'strava'?", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-providers.adoc", "range": {"start": {"line": 13, "column": 104}}}, "severity": "WARNING"}
:extensions: io.quarkus:quarkus-oidc

This document explains how to configure well-known social OIDC and OAuth2 providers.
Expand Down Expand Up @@ -525,7 +525,23 @@ quarkus.oidc.client-id=<Client ID>
quarkus.oidc.credentials.client-secret=<Client Secret>
----

[[strava]]
=== Strava

Check warning on line 529 in docs/src/main/asciidoc/security-openid-connect-providers.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Spelling] Use correct American English spelling. Did you really mean 'Strava'? Raw Output: {"message": "[Quarkus.Spelling] Use correct American English spelling. Did you really mean 'Strava'?", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-providers.adoc", "range": {"start": {"line": 529, "column": 5}}}, "severity": "WARNING"}

Create a https://www.strava.com/settings/api[Strava application]:

image::oidc-strava-1.png[role="thumb"]

For example, set `Category` to `SocialMotivation`, and set `ApplicationCallbackDomain` either to `localhost` or the domain name provided by Ngrok, see the <<redirect_url>> for more information.

Check warning on line 535 in docs/src/main/asciidoc/security-openid-connect-providers.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Spelling] Use correct American English spelling. Did you really mean 'Ngrok'? Raw Output: {"message": "[Quarkus.Spelling] Use correct American English spelling. Did you really mean 'Ngrok'?", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-providers.adoc", "range": {"start": {"line": 535, "column": 141}}}, "severity": "WARNING"}

You can now configure your `application.properties`:

[source,properties]
----
quarkus.oidc.provider=strava
quarkus.oidc.client-id=<Client ID>
quarkus.oidc.credentials.client-secret=<Client Secret>
----

[[provider-scope]]
== Provider scopes
Expand Down Expand Up @@ -685,6 +701,7 @@ Follow the same approach if the endpoint must access other Google services.

The pattern of authenticating with a given provider, where the endpoint uses either an ID token or UserInfo (especially if an OAuth2-only provider such as `GitHub` is used) to get some information about the currently authenticated user and using an access token to access some downstream services (provider or application specific ones) on behalf of this user can be universally applied, irrespectively of which provider is used to secure the application.

Check warning on line 702 in docs/src/main/asciidoc/security-openid-connect-providers.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer. Raw Output: {"message": "[Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer.", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-providers.adoc", "range": {"start": {"line": 702, "column": 1}}}, "severity": "INFO"}

Check warning on line 702 in docs/src/main/asciidoc/security-openid-connect-providers.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'.", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-providers.adoc", "range": {"start": {"line": 702, "column": 240}}}, "severity": "INFO"}

[[redirect_url]]
== HTTPS Redirect URL

Some providers will only accept HTTPS-based redirect URLs. Tools such as https://ngrok.com/[ngrok] https://linuxhint.com/set-up-use-ngrok/[can be set up] to help testing such providers with Quarkus endpoints running on localhost in dev mode.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,12 @@ public static enum Method {
* form
* parameters.
*/
POST_JWT
POST_JWT,

/**
* client id and secret are submitted as HTTP query parameters.
*/
QUERY
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1812,6 +1812,7 @@ public static enum Provider {
MASTODON,
MICROSOFT,
SPOTIFY,
STRAVA,
TWITCH,
TWITTER,
// New name for Twitter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import io.quarkus.oidc.TokenCustomizer;
import io.quarkus.oidc.TokenIntrospection;
import io.quarkus.oidc.UserInfo;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.credential.TokenCredential;
Expand Down Expand Up @@ -551,7 +552,7 @@ private class SymmetricKeyResolver implements VerificationKeyResolver {
@Override
public Key resolveKey(JsonWebSignature jws, List<JsonWebStructure> nestingContext)
throws UnresolvableKeyException {
return KeyUtils.createSecretKeyFromSecret(oidcConfig.credentials.secret.get());
return KeyUtils.createSecretKeyFromSecret(OidcCommonUtils.clientSecret(oidcConfig.credentials));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Secret.Method;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.oidc.common.runtime.OidcEndpointAccessException;
Expand Down Expand Up @@ -51,6 +52,7 @@ public class OidcProviderClient implements Closeable {
private final String introspectionBasicAuthScheme;
private final Key clientJwtKey;
private final Map<OidcEndpoint.Type, List<OidcRequestFilter>> filters;
private final boolean clientSecretQueryAuthentication;

public OidcProviderClient(WebClient client,
Vertx vertx,
Expand All @@ -65,6 +67,7 @@ public OidcProviderClient(WebClient client,
this.clientJwtKey = OidcCommonUtils.initClientJwtKey(oidcConfig);
this.introspectionBasicAuthScheme = initIntrospectionBasicAuthScheme(oidcConfig);
this.filters = filters;
this.clientSecretQueryAuthentication = oidcConfig.credentials.clientSecret.method.orElse(null) == Method.QUERY;
}

private static String initIntrospectionBasicAuthScheme(OidcTenantConfig oidcConfig) {
Expand Down Expand Up @@ -139,45 +142,62 @@ public Uni<AuthorizationCodeTokens> refreshAuthorizationCodeTokens(String refres

private UniOnItem<HttpResponse<Buffer>> getHttpResponse(String uri, MultiMap formBody, boolean introspect) {
HttpRequest<Buffer> request = client.postAbs(uri);
request.putHeader(CONTENT_TYPE_HEADER, APPLICATION_X_WWW_FORM_URLENCODED);
request.putHeader(ACCEPT_HEADER, APPLICATION_JSON);
if (oidcConfig.codeGrant.headers != null) {
for (Map.Entry<String, String> headerEntry : oidcConfig.codeGrant.headers.entrySet()) {
request.putHeader(headerEntry.getKey(), headerEntry.getValue());
}
}
if (introspect && introspectionBasicAuthScheme != null) {
request.putHeader(AUTHORIZATION_HEADER, introspectionBasicAuthScheme);
if (oidcConfig.clientId.isPresent() && oidcConfig.introspectionCredentials.includeClientId) {
formBody.set(OidcConstants.CLIENT_ID, oidcConfig.clientId.get());
}
} else if (clientSecretBasicAuthScheme != null) {
request.putHeader(AUTHORIZATION_HEADER, clientSecretBasicAuthScheme);
} else if (clientJwtKey != null) {
String jwt = OidcCommonUtils.signJwtWithKey(oidcConfig, metadata.getTokenUri(), clientJwtKey);
if (OidcCommonUtils.isClientSecretPostJwtAuthRequired(oidcConfig.credentials)) {

Buffer buffer = null;

if (!clientSecretQueryAuthentication) {
request.putHeader(CONTENT_TYPE_HEADER, APPLICATION_X_WWW_FORM_URLENCODED);
request.putHeader(ACCEPT_HEADER, APPLICATION_JSON);

if (introspect && introspectionBasicAuthScheme != null) {
request.putHeader(AUTHORIZATION_HEADER, introspectionBasicAuthScheme);
if (oidcConfig.clientId.isPresent() && oidcConfig.introspectionCredentials.includeClientId) {
formBody.set(OidcConstants.CLIENT_ID, oidcConfig.clientId.get());
}
} else if (clientSecretBasicAuthScheme != null) {
request.putHeader(AUTHORIZATION_HEADER, clientSecretBasicAuthScheme);
} else if (clientJwtKey != null) {
String jwt = OidcCommonUtils.signJwtWithKey(oidcConfig, metadata.getTokenUri(), clientJwtKey);
if (OidcCommonUtils.isClientSecretPostJwtAuthRequired(oidcConfig.credentials)) {
formBody.add(OidcConstants.CLIENT_ID, oidcConfig.clientId.get());
formBody.add(OidcConstants.CLIENT_SECRET, jwt);
} else {
formBody.add(OidcConstants.CLIENT_ASSERTION_TYPE, OidcConstants.JWT_BEARER_CLIENT_ASSERTION_TYPE);
formBody.add(OidcConstants.CLIENT_ASSERTION, jwt);
}
} else if (OidcCommonUtils.isClientSecretPostAuthRequired(oidcConfig.credentials)) {
formBody.add(OidcConstants.CLIENT_ID, oidcConfig.clientId.get());
formBody.add(OidcConstants.CLIENT_SECRET, jwt);
formBody.add(OidcConstants.CLIENT_SECRET, OidcCommonUtils.clientSecret(oidcConfig.credentials));
} else {
formBody.add(OidcConstants.CLIENT_ASSERTION_TYPE, OidcConstants.JWT_BEARER_CLIENT_ASSERTION_TYPE);
formBody.add(OidcConstants.CLIENT_ASSERTION, jwt);
formBody.add(OidcConstants.CLIENT_ID, oidcConfig.clientId.get());
}
} else if (OidcCommonUtils.isClientSecretPostAuthRequired(oidcConfig.credentials)) {
formBody.add(OidcConstants.CLIENT_ID, oidcConfig.clientId.get());
formBody.add(OidcConstants.CLIENT_SECRET, OidcCommonUtils.clientSecret(oidcConfig.credentials));
buffer = OidcCommonUtils.encodeForm(formBody);
} else {
formBody.add(OidcConstants.CLIENT_ID, oidcConfig.clientId.get());
formBody.add(OidcConstants.CLIENT_SECRET, OidcCommonUtils.clientSecret(oidcConfig.credentials));
for (Map.Entry<String, String> entry : formBody) {
request.addQueryParam(entry.getKey(), OidcCommonUtils.urlEncode(entry.getValue()));
}
request.putHeader(ACCEPT_HEADER, APPLICATION_JSON);
buffer = Buffer.buffer();
}

if (oidcConfig.codeGrant.headers != null) {
for (Map.Entry<String, String> headerEntry : oidcConfig.codeGrant.headers.entrySet()) {
request.putHeader(headerEntry.getKey(), headerEntry.getValue());
}
}

LOG.debugf("Get token on: %s params: %s headers: %s", metadata.getTokenUri(), formBody, request.headers());
// Retry up to three times with a one-second delay between the retries if the connection is closed.
Buffer buffer = OidcCommonUtils.encodeForm(formBody);

OidcEndpoint.Type endpoint = introspect ? OidcEndpoint.Type.INTROSPECTION : OidcEndpoint.Type.TOKEN;
Uni<HttpResponse<Buffer>> response = filter(endpoint, request, buffer, null).sendBuffer(buffer)
.onFailure(ConnectException.class)
.retry()
.atMost(oidcConfig.connectionRetryCount).onFailure().transform(t -> t.getCause());
return response.onItem();

}

private AuthorizationCodeTokens getAuthorizationCodeTokens(HttpResponse<Buffer> resp) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public static OidcTenantConfig provider(OidcTenantConfig.Provider provider) {
case MASTODON -> mastodon();
case MICROSOFT -> microsoft();
case SPOTIFY -> spotify();
case STRAVA -> strava();
case TWITCH -> twitch();
case TWITTER, X -> twitter();
};
Expand Down Expand Up @@ -153,6 +154,27 @@ private static OidcTenantConfig spotify() {
return ret;
}

private static OidcTenantConfig strava() {
OidcTenantConfig ret = new OidcTenantConfig();
ret.setDiscoveryEnabled(false);
ret.setAuthServerUrl("https://www.strava.com/oauth");
ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP);
ret.setAuthorizationPath("authorize");

ret.setTokenPath("token");
ret.setUserInfoPath("https://www.strava.com/api/v3/athlete");

OidcTenantConfig.Authentication authentication = ret.getAuthentication();
authentication.setAddOpenidScope(false);
authentication.setScopes(List.of("activity:read"));
authentication.setIdTokenRequired(false);

ret.getToken().setVerifyAccessTokenWithUserInfo(true);
ret.getCredentials().getClientSecret().setMethod(Method.QUERY);

return ret;
}

private static OidcTenantConfig twitch() {
// Ref https://dev.twitch.tv/docs/authentication/getting-tokens-oidc/#oidc-authorization-code-grant-flow

Expand Down

0 comments on commit dc520ca

Please sign in to comment.