Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions core/src/main/java/ch/cyberduck/core/AbstractProtocol.java
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,11 @@ public String getOAuthAuthorizationUrl() {
return null;
}

@Override
public String getOAuthUserInfoUrl() {
return null;
}

@Override
public String getOAuthTokenUrl() {
return null;
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/java/ch/cyberduck/core/Profile.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/java/ch/cyberduck/core/Protocol.java
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ public interface Protocol extends FeatureFactory, Comparable<Protocol>, Serializ
*/
String getOAuthAuthorizationUrl();

String getOAuthUserInfoUrl();

/**
* @return OAuth 2 Token Server URL
*/
Expand Down
2 changes: 2 additions & 0 deletions eue/src/test/resources/mac/GMX Cloud.cyberduckprofile
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
<false/>
<key>OAuth Authorization Url</key>
<string>https://oauth2.gmx.net/authorize</string>
<key>OAuth User Info Url</key>
<string>https://um-data-facade.gmx.net/userinfo</string>
<key>OAuth Token Url</key>
<string>https://oauth2.gmx.net/token</string>
<key>OAuth Redirect Url</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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<String> 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<String> 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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand All @@ -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<String> 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading