diff --git a/core/src/main/java/ch/cyberduck/core/AbstractProtocol.java b/core/src/main/java/ch/cyberduck/core/AbstractProtocol.java index 7d5475bf5f9..d608d1c3184 100644 --- a/core/src/main/java/ch/cyberduck/core/AbstractProtocol.java +++ b/core/src/main/java/ch/cyberduck/core/AbstractProtocol.java @@ -406,6 +406,11 @@ public String getOAuthAuthorizationUrl() { return null; } + @Override + public String getOAuthUserInfoUrl() { + return null; + } + @Override public String getOAuthTokenUrl() { return null; diff --git a/core/src/main/java/ch/cyberduck/core/Profile.java b/core/src/main/java/ch/cyberduck/core/Profile.java index 1d65fca6432..838719b587d 100644 --- a/core/src/main/java/ch/cyberduck/core/Profile.java +++ b/core/src/main/java/ch/cyberduck/core/Profile.java @@ -62,6 +62,7 @@ public class Profile implements Protocol { public static final String OAUTH_TOKEN_URL_KEY = "OAuth Token Url"; public static final String OAUTH_REDIRECT_URL_KEY = "OAuth Redirect Url"; public static final String OAUTH_AUTHORIZATION_URL_KEY = "OAuth Authorization Url"; + public static final String OAUTH_USERINFO_URL_KEY = "OAuth User Info Url"; public static final String OAUTH_PKCE_KEY = "OAuth PKCE"; public static final String SCOPES_KEY = "Scopes"; public static final String STS_ENDPOINT_KEY = "STS Endpoint"; @@ -627,6 +628,15 @@ public String getOAuthAuthorizationUrl() { return v; } + @Override + public String getOAuthUserInfoUrl() { + final String v = this.value(OAUTH_USERINFO_URL_KEY); + if(StringUtils.isBlank(v)) { + return parent.getOAuthUserInfoUrl(); + } + return v; + } + @Override public String getOAuthTokenUrl() { final String v = this.value(OAUTH_TOKEN_URL_KEY); diff --git a/core/src/main/java/ch/cyberduck/core/Protocol.java b/core/src/main/java/ch/cyberduck/core/Protocol.java index b67947b9117..70b03a935cc 100644 --- a/core/src/main/java/ch/cyberduck/core/Protocol.java +++ b/core/src/main/java/ch/cyberduck/core/Protocol.java @@ -257,6 +257,8 @@ public interface Protocol extends FeatureFactory, Comparable, Serializ */ String getOAuthAuthorizationUrl(); + String getOAuthUserInfoUrl(); + /** * @return OAuth 2 Token Server URL */ diff --git a/eue/src/test/resources/mac/GMX Cloud.cyberduckprofile b/eue/src/test/resources/mac/GMX Cloud.cyberduckprofile index 9ab7ac01745..5985a742604 100644 --- a/eue/src/test/resources/mac/GMX Cloud.cyberduckprofile +++ b/eue/src/test/resources/mac/GMX Cloud.cyberduckprofile @@ -54,6 +54,8 @@ OAuth Authorization Url https://oauth2.gmx.net/authorize + OAuth User Info Url + https://um-data-facade.gmx.net/userinfo OAuth Token Url https://oauth2.gmx.net/token OAuth Redirect Url diff --git a/oauth/src/main/java/ch/cyberduck/core/oauth/OAuth2AuthorizationService.java b/oauth/src/main/java/ch/cyberduck/core/oauth/OAuth2AuthorizationService.java index f131b6c8ebb..108cc234c58 100644 --- a/oauth/src/main/java/ch/cyberduck/core/oauth/OAuth2AuthorizationService.java +++ b/oauth/src/main/java/ch/cyberduck/core/oauth/OAuth2AuthorizationService.java @@ -63,6 +63,7 @@ import com.google.api.client.auth.openidconnect.IdTokenResponse; import com.google.api.client.http.BasicAuthentication; import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.apache.v2.ApacheHttpTransport; @@ -86,6 +87,7 @@ public class OAuth2AuthorizationService { private String clientsecret; private String tokenServerUrl; private String authorizationServerUrl; + private final String userinfoUrl; private String redirectUri = OOB_REDIRECT_URI; private FlowType flowType = FlowType.AuthorizationCode; @@ -103,21 +105,22 @@ public class OAuth2AuthorizationService { private final HostPasswordStore store = PasswordStoreFactory.get(); public OAuth2AuthorizationService(final HttpClient client, final Host host, - final String tokenServerUrl, final String authorizationServerUrl, + final String tokenServerUrl, final String authorizationServerUrl, final String userinfoUrl, final String clientid, final String clientsecret, final List scopes, final boolean pkce, final LoginCallback prompt) { this(new ApacheHttpTransport(client), host, - tokenServerUrl, authorizationServerUrl, clientid, clientsecret, scopes, pkce, prompt); + tokenServerUrl, authorizationServerUrl, userinfoUrl, clientid, clientsecret, scopes, pkce, prompt); } public OAuth2AuthorizationService(final HttpTransport transport, final Host host, - final String tokenServerUrl, final String authorizationServerUrl, + final String tokenServerUrl, final String authorizationServerUrl, final String userinfoUrl, final String clientid, final String clientsecret, final List scopes, final boolean pkce, final LoginCallback prompt) { this.transport = transport; this.host = host; this.tokenServerUrl = tokenServerUrl; this.authorizationServerUrl = authorizationServerUrl; + this.userinfoUrl = userinfoUrl; this.prompt = prompt; this.clientid = clientid; this.clientsecret = clientsecret; @@ -154,23 +157,12 @@ public OAuthTokens validate(final OAuthTokens saved) throws BackgroundException log.warn("Missing tokens {} for {}", saved, host); final OAuthTokens tokens = this.authorize(); log.debug("Retrieved tokens {} for {}", tokens, host); - return tokens; - } - - /** - * Save updated tokens in keychain - * - * @return Same tokens saved - */ - public OAuthTokens save(final OAuthTokens tokens) throws AccessDeniedException { - log.debug("Save new tokens {} for {}", tokens, host); - final Credentials credentials = host.getCredentials(); - credentials.setOauth(tokens).setSaved(new LoginOptions().save); switch(flowType) { case PasswordGrant: // Skip modifying username used for password grant break; default: + final Credentials credentials = host.getCredentials(); if(StringUtils.isBlank(credentials.getUsername())) { if(null != tokens.getIdToken()) { try { @@ -189,9 +181,48 @@ public OAuthTokens save(final OAuthTokens tokens) throws AccessDeniedException { log.warn("Failure {} decoding JWT {}", e, tokens.getIdToken()); } } + else { + if(userinfoUrl != null) { + try { + // https://openid.net/specs/openid-connect-core-1_0.html#UserInfo + log.debug("Request user info from UserInfo endpoint {}", userinfoUrl); + final HttpRequest request = transport.createRequestFactory() + .buildGetRequest(new GenericUrl(userinfoUrl)); + request.getHeaders().setAuthorization(String.format("Bearer %s", tokens.getAccessToken())); + request.setParser(json.createJsonObjectParser()); + final GenericJson response = request.execute().parseAs(GenericJson.class); + log.debug("Received user info response {}", response); + // Parse standard claims as per Section 5.3.2 + for(String claim : new String[]{"preferred_username", "email", "name", "nickname", "sub"}) { + final Object value = response.get(claim); + if(null == value) { + continue; + } + log.debug("Set username to {} from claim {}", value, claim); + credentials.setUsername(value.toString()); + break; + } + } + catch(IOException e) { + log.warn("Failure {} fetching user info from UserInfo endpoint {}", e, userinfoUrl); + } + } + } } break; } + return tokens; + } + + /** + * Save updated tokens in keychain + * + * @return Same tokens saved + */ + public OAuthTokens save(final OAuthTokens tokens) throws AccessDeniedException { + log.debug("Save new tokens {} for {}", tokens, host); + final Credentials credentials = host.getCredentials(); + credentials.setOauth(tokens).setSaved(new LoginOptions().save); if(credentials.isSaved()) { store.save(host); } diff --git a/oauth/src/main/java/ch/cyberduck/core/oauth/OAuth2RequestInterceptor.java b/oauth/src/main/java/ch/cyberduck/core/oauth/OAuth2RequestInterceptor.java index e801fe58dba..39612e5b8c9 100644 --- a/oauth/src/main/java/ch/cyberduck/core/oauth/OAuth2RequestInterceptor.java +++ b/oauth/src/main/java/ch/cyberduck/core/oauth/OAuth2RequestInterceptor.java @@ -54,6 +54,7 @@ public OAuth2RequestInterceptor(final HttpClient client, final Host host, final host.getProtocol().getScheme(), host.getPort(), null, host.getHostname(), host.getProtocol().getOAuthTokenUrl()), Scheme.isURL(host.getProtocol().getOAuthAuthorizationUrl()) ? host.getProtocol().getOAuthAuthorizationUrl() : new HostUrlProvider().withUsername(false).withPath(true).get( host.getProtocol().getScheme(), host.getPort(), null, host.getHostname(), host.getProtocol().getOAuthAuthorizationUrl()), + host.getProtocol().getOAuthUserInfoUrl(), prompt(host, prompt, Profile.OAUTH_CLIENT_ID_KEY, LocaleFactory.localizedString( Profile.OAUTH_CLIENT_ID_KEY, "Credentials"), null == host.getProperty(Profile.OAUTH_CLIENT_ID_KEY) ? host.getProtocol().getOAuthClientId() : host.getProperty(Profile.OAUTH_CLIENT_ID_KEY)), @@ -64,9 +65,9 @@ public OAuth2RequestInterceptor(final HttpClient client, final Host host, final host.getProtocol().isOAuthPKCE(), prompt); } - public OAuth2RequestInterceptor(final HttpClient client, final Host host, final String tokenServerUrl, final String authorizationServerUrl, + public OAuth2RequestInterceptor(final HttpClient client, final Host host, final String tokenServerUrl, final String authorizationServerUrl, final String userInfoUrl, final String clientid, final String clientsecret, final List scopes, final boolean pkce, final LoginCallback prompt) throws LoginCanceledException { - super(client, host, tokenServerUrl, authorizationServerUrl, clientid, clientsecret, scopes, pkce, prompt); + super(client, host, tokenServerUrl, authorizationServerUrl, userInfoUrl, clientid, clientsecret, scopes, pkce, prompt); this.host = host; } diff --git a/s3/src/main/java/ch/cyberduck/core/sso/RegisterClientOAuth2RequestInterceptor.java b/s3/src/main/java/ch/cyberduck/core/sso/RegisterClientOAuth2RequestInterceptor.java index 6045436657e..87480fe8972 100644 --- a/s3/src/main/java/ch/cyberduck/core/sso/RegisterClientOAuth2RequestInterceptor.java +++ b/s3/src/main/java/ch/cyberduck/core/sso/RegisterClientOAuth2RequestInterceptor.java @@ -82,7 +82,7 @@ public class RegisterClientOAuth2RequestInterceptor extends OAuth2RequestInterce public RegisterClientOAuth2RequestInterceptor(final HttpClient client, final Host host, final X509TrustManager trust, final X509KeyManager key, final LoginCallback prompt) throws ConnectionCanceledException { - super(client, host, null, null, null, null, host.getProtocol().getOAuthScopes(), true, prompt); + super(client, host, null, null, null, null, null, host.getProtocol().getOAuthScopes(), true, prompt); this.host = host; if(StringUtils.isBlank(host.getCredentials().getUsername())) { final S3CredentialsConfigurator configurator = new S3CredentialsConfigurator();