Skip to content

Commit

Permalink
Merge pull request #24774 from FroMage/23593
Browse files Browse the repository at this point in the history
Add Twitter OIDC provider configuration
  • Loading branch information
sberyozkin authored Apr 6, 2022
2 parents 5f888b4 + 8d02d6d commit 3947967
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,13 @@ public enum ResponseMode {
@ConfigItem
public Optional<List<String>> scopes = Optional.empty();

/**
* Add the 'openid' scope automatically to the list of scopes. This is required for OpenId Connect providers
* but will not work for OAuth2 providers such as Twitter OAuth2 which does not accept that scope and throws an error.
*/
@ConfigItem(defaultValueDocumentation = "true")
public Optional<Boolean> addOpenidScope = Optional.empty();

/**
* Additional properties which will be added as the query parameters to the authentication redirect URI.
*/
Expand Down Expand Up @@ -721,6 +728,14 @@ public void setExtraParams(Map<String, String> extraParams) {
this.extraParams = extraParams;
}

public void setAddOpenidScope(boolean addOpenidScope) {
this.addOpenidScope = Optional.of(addOpenidScope);
}

public Optional<Boolean> isAddOpenidScope() {
return addOpenidScope;
}

public Optional<Boolean> isForceRedirectHttpsScheme() {
return forceRedirectHttpsScheme;
}
Expand Down Expand Up @@ -1065,7 +1080,8 @@ public static enum Provider {
FACEBOOK,
GITHUB,
GOOGLE,
MICROSOFT
MICROSOFT,
TWITTER
}

public Optional<Provider> getProvider() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,9 +350,10 @@ && isRedirectFromProvider(context, configContext)) {
? configContext.oidcConfig.getAuthentication().scopes.get()
: Collections.emptyList();
List<String> scopes = new ArrayList<>(oidcConfigScopes.size() + 1);
scopes.add("openid");
if (configContext.oidcConfig.getAuthentication().addOpenidScope.orElse(true)) {
scopes.add("openid");
}
scopes.addAll(oidcConfigScopes);
configContext.oidcConfig.getAuthentication().scopes.ifPresent(scopes::addAll);
codeFlowParams.append(AMP).append(OidcConstants.TOKEN_SCOPE).append(EQ)
.append(OidcCommonUtils.urlEncode(String.join(" ", scopes)));

Expand Down Expand Up @@ -387,6 +388,8 @@ && isRedirectFromProvider(context, configContext)) {
String authorizationURL = configContext.provider.getMetadata().getAuthorizationUri() + "?"
+ codeFlowParams.toString();

LOG.debugf("Code flow redirect to: %s", authorizationURL);

return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.FOUND.code(), HttpHeaders.LOCATION,
authorizationURL));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public Uni<JsonWebKeySet> getJsonWebKeySet() {
}

public Uni<UserInfo> getUserInfo(String token) {
LOG.debugf("Get UserInfo on: %s auth: %s", metadata.getUserInfoUri(), OidcConstants.BEARER_SCHEME + " " + token);
return client.getAbs(metadata.getUserInfoUri())
.putHeader(AUTHORIZATION_HEADER, OidcConstants.BEARER_SCHEME + " " + token)
.send().onItem().transform(resp -> getUserInfo(resp));
Expand Down Expand Up @@ -126,6 +127,7 @@ private UniOnItem<HttpResponse<Buffer>> getHttpResponse(String uri, MultiMap for
} else {
formBody.add(OidcConstants.CLIENT_ID, oidcConfig.clientId.get());
}
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.
Uni<HttpResponse<Buffer>> response = request.sendBuffer(OidcCommonUtils.encodeForm(formBody))
.onFailure(ConnectException.class)
Expand All @@ -152,6 +154,7 @@ private TokenIntrospection getTokenIntrospection(HttpResponse<Buffer> resp) {

private static JsonObject getJsonObject(HttpResponse<Buffer> resp) {
if (resp.statusCode() == 200) {
LOG.debugf("Request succeeded: %s", resp.bodyAsJsonObject());
return resp.bodyAsJsonObject();
} else {
throw responseException(resp);
Expand All @@ -160,6 +163,7 @@ private static JsonObject getJsonObject(HttpResponse<Buffer> resp) {

private static String getString(HttpResponse<Buffer> resp) {
if (resp.statusCode() == 200) {
LOG.debugf("Request succeeded: %s", resp.bodyAsString());
return resp.bodyAsString();
} else {
throw responseException(resp);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,9 +353,15 @@ static OidcTenantConfig mergeTenantConfig(OidcTenantConfig tenant, OidcTenantCon
if (tenant.authentication.userInfoRequired.isEmpty()) {
tenant.authentication.userInfoRequired = provider.authentication.userInfoRequired;
}
if (tenant.authentication.pkceRequired.isEmpty()) {
tenant.authentication.pkceRequired = provider.authentication.pkceRequired;
}
if (tenant.authentication.scopes.isEmpty()) {
tenant.authentication.scopes = provider.authentication.scopes;
}
if (tenant.authentication.addOpenidScope.isEmpty()) {
tenant.authentication.addOpenidScope = provider.authentication.addOpenidScope;
}
if (tenant.authentication.forceRedirectHttpsScheme.isEmpty()) {
tenant.authentication.forceRedirectHttpsScheme = provider.authentication.forceRedirectHttpsScheme;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public static OidcTenantConfig provider(OidcTenantConfig.Provider provider) {
return microsoft();
} else if (OidcTenantConfig.Provider.FACEBOOK == provider) {
return facebook();
} else if (OidcTenantConfig.Provider.TWITTER == provider) {
return twitter();
}
return null;
}
Expand All @@ -38,6 +40,22 @@ private static OidcTenantConfig github() {
return ret;
}

private static OidcTenantConfig twitter() {
OidcTenantConfig ret = new OidcTenantConfig();
ret.setAuthServerUrl("https://api.twitter.com/2/oauth2");
ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP);
ret.setDiscoveryEnabled(false);
ret.setAuthorizationPath("https://twitter.com/i/oauth2/authorize");
ret.setTokenPath("token");
ret.setUserInfoPath("https://api.twitter.com/2/users/me");
ret.getAuthentication().setAddOpenidScope(false);
ret.getAuthentication().setScopes(List.of("offline.access", "tweet.read", "users.read"));
ret.getAuthentication().setUserInfoRequired(true);
ret.getAuthentication().setIdTokenRequired(false);
ret.getAuthentication().setPkceRequired(true);
return ret;
}

private static OidcTenantConfig google() {
OidcTenantConfig ret = new OidcTenantConfig();
ret.setAuthServerUrl("https://accounts.google.com");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,62 @@ public void testOverrideGitHubProperties() throws Exception {
assertEquals(List.of("write"), config.authentication.scopes.get());
}

@Test
public void testAcceptTwitterProperties() throws Exception {
OidcTenantConfig tenant = new OidcTenantConfig();
tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID);
OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.TWITTER));

assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get());
assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get());
assertFalse(config.isDiscoveryEnabled().get());
assertEquals("https://api.twitter.com/2/oauth2", config.getAuthServerUrl().get());
assertEquals("https://twitter.com/i/oauth2/authorize", config.getAuthorizationPath().get());
assertEquals("token", config.getTokenPath().get());
assertEquals("https://api.twitter.com/2/users/me", config.getUserInfoPath().get());

assertFalse(config.authentication.idTokenRequired.get());
assertTrue(config.authentication.userInfoRequired.get());
assertFalse(config.authentication.addOpenidScope.get());
assertEquals(List.of("offline.access", "tweet.read", "users.read"), config.authentication.scopes.get());
assertTrue(config.authentication.pkceRequired.get());
}

@Test
public void testOverrideTwitterProperties() throws Exception {
OidcTenantConfig tenant = new OidcTenantConfig();
tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID);

tenant.setApplicationType(ApplicationType.HYBRID);
tenant.setDiscoveryEnabled(true);
tenant.setAuthServerUrl("http://localhost/wiremock");
tenant.setAuthorizationPath("authorization");
tenant.setTokenPath("tokens");
tenant.setUserInfoPath("userinfo");

tenant.authentication.setIdTokenRequired(true);
tenant.authentication.setUserInfoRequired(false);
tenant.authentication.setAddOpenidScope(true);
tenant.authentication.setPkceRequired(false);
tenant.authentication.setScopes(List.of("write"));

OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.TWITTER));

assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get());
assertEquals(ApplicationType.HYBRID, config.getApplicationType().get());
assertTrue(config.isDiscoveryEnabled().get());
assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get());
assertEquals("authorization", config.getAuthorizationPath().get());
assertEquals("tokens", config.getTokenPath().get());
assertEquals("userinfo", config.getUserInfoPath().get());

assertTrue(config.authentication.idTokenRequired.get());
assertFalse(config.authentication.userInfoRequired.get());
assertEquals(List.of("write"), config.authentication.scopes.get());
assertTrue(config.authentication.addOpenidScope.get());
assertFalse(config.authentication.pkceRequired.get());
}

@Test
public void testAcceptFacebookProperties() throws Exception {
OidcTenantConfig tenant = new OidcTenantConfig();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public void testCodeFlowScopeErrorWithErrorPage() throws IOException {
endpointErrorLocation = "http" + endpointErrorLocation.substring(5);

HtmlPage page = webClient.getPage(URI.create(endpointErrorLocation).toURL());
assertEquals("error: invalid_scope, error_description: Invalid scopes: unknown profile email phone",
assertEquals("error: invalid_scope, error_description: Invalid scopes: unknown",
page.getBody().asText());
webClient.getCookieManager().clearCookies();
}
Expand Down

0 comments on commit 3947967

Please sign in to comment.