44
55package cloud .katta .protocols .s3 ;
66
7+ import ch .cyberduck .core .Credentials ;
78import ch .cyberduck .core .DefaultIOExceptionMappingService ;
89import ch .cyberduck .core .Host ;
910import ch .cyberduck .core .LoginCallback ;
1011import ch .cyberduck .core .OAuthTokens ;
1112import ch .cyberduck .core .exception .BackgroundException ;
1213import ch .cyberduck .core .exception .LoginCanceledException ;
14+ import ch .cyberduck .core .exception .LoginFailureException ;
1315import ch .cyberduck .core .http .DefaultHttpResponseExceptionMappingService ;
1416import ch .cyberduck .core .oauth .OAuth2RequestInterceptor ;
1517import ch .cyberduck .core .oauth .OAuthExceptionMappingService ;
2426import java .io .IOException ;
2527import java .util .ArrayList ;
2628import java .util .Arrays ;
29+ import java .util .List ;
2730
31+ import com .auth0 .jwt .JWT ;
32+ import com .auth0 .jwt .exceptions .JWTDecodeException ;
33+ import com .auth0 .jwt .interfaces .DecodedJWT ;
2834import com .google .api .client .auth .oauth2 .TokenRequest ;
2935import com .google .api .client .auth .oauth2 .TokenResponse ;
3036import com .google .api .client .auth .oauth2 .TokenResponseException ;
3339import com .google .api .client .http .apache .v2 .ApacheHttpTransport ;
3440import com .google .api .client .json .gson .GsonFactory ;
3541
42+ import static cloud .katta .protocols .s3 .S3AssumeRoleProtocol .OAUTH_TOKENEXCHANGE ;
43+
3644/**
37- * Exchange OIDC token to scoped token using OAuth 2.0 Token Exchange
45+ * Exchange OIDC token to scoped token using OAuth 2.0 Token Exchange. Used for S3-STS in Katta.
3846 */
3947public class TokenExchangeRequestInterceptor extends OAuth2RequestInterceptor {
4048 private static final Logger log = LogManager .getLogger (TokenExchangeRequestInterceptor .class );
4149
4250 // https://datatracker.ietf.org/doc/html/rfc8693#name-request
4351 public static final String OAUTH_GRANT_TYPE_TOKEN_EXCHANGE = "urn:ietf:params:oauth:grant-type:token-exchange" ;
4452 public static final String OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_CLIENT_ID = "client_id" ;
45- public static final String OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_AUDIENCE = "audience " ;
53+ public static final String OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_CLIENT_SECRET = "client_secret " ;
4654 public static final String OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_SUBJECT_TOKEN = "subject_token" ;
55+ public static final String OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_SUBJECT_TOKEN_TYPE = "subject_token_type" ;
56+ public static final String OAUTH_TOKEN_TYPE_ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token" ;
57+ // https://openid.net/specs/openid-connect-core-1_0.html
58+ public static final String OIDC_AUTHORIZED_PARTY = "azp" ;
59+
4760
4861 private final Host bookmark ;
4962 private final HttpClient client ;
@@ -69,8 +82,7 @@ public OAuthTokens refresh(final OAuthTokens previous) throws BackgroundExceptio
6982 *
7083 * @param previous Input tokens retrieved to exchange at the token endpoint
7184 * @return New tokens
72- *
73- * @see S3AssumeRoleProtocol#OAUTH_TOKENEXCHANGE_AUDIENCE
85+ * @see S3AssumeRoleProtocol#OAUTH_TOKENEXCHANGE_CLIENT_ID
7486 * @see S3AssumeRoleProtocol#OAUTH_TOKENEXCHANGE_ADDITIONAL_SCOPES
7587 */
7688 public OAuthTokens exchange (final OAuthTokens previous ) throws BackgroundException {
@@ -83,10 +95,12 @@ public OAuthTokens exchange(final OAuthTokens previous) throws BackgroundExcepti
8395 );
8496 request .set (OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_CLIENT_ID , bookmark .getProtocol ().getOAuthClientId ());
8597 final PreferencesReader preferences = new HostPreferences (bookmark );
86- if (!StringUtils .isEmpty (preferences .getProperty (S3AssumeRoleProtocol .OAUTH_TOKENEXCHANGE_AUDIENCE ))) {
87- request .set (OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_AUDIENCE , preferences .getProperty (S3AssumeRoleProtocol .OAUTH_TOKENEXCHANGE_AUDIENCE ));
98+ if (!StringUtils .isEmpty (preferences .getProperty (S3AssumeRoleProtocol .OAUTH_TOKENEXCHANGE_CLIENT_ID ))) {
99+ request .set (OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_CLIENT_ID , preferences .getProperty (S3AssumeRoleProtocol .OAUTH_TOKENEXCHANGE_CLIENT_ID ));
100+ request .set (OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_CLIENT_SECRET , preferences .getProperty (S3AssumeRoleProtocol .OAUTH_TOKENEXCHANGE_CLIENT_SECRET ));
88101 }
89102 request .set (OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_SUBJECT_TOKEN , previous .getAccessToken ());
103+ request .set (OAUTH_GRANT_TYPE_TOKEN_EXCHANGE_SUBJECT_TOKEN_TYPE , OAUTH_TOKEN_TYPE_ACCESS_TOKEN );
90104 final ArrayList <String > scopes = new ArrayList <>(bookmark .getProtocol ().getOAuthScopes ());
91105 if (!StringUtils .isEmpty (preferences .getProperty (S3AssumeRoleProtocol .OAUTH_TOKENEXCHANGE_ADDITIONAL_SCOPES ))) {
92106 scopes .addAll (Arrays .asList (preferences .getProperty (S3AssumeRoleProtocol .OAUTH_TOKENEXCHANGE_ADDITIONAL_SCOPES ).split (" " )));
@@ -113,4 +127,34 @@ public OAuthTokens exchange(final OAuthTokens previous) throws BackgroundExcepti
113127 throw new DefaultIOExceptionMappingService ().map (e );
114128 }
115129 }
130+
131+ @ Override
132+ public Credentials validate () throws BackgroundException {
133+ final Credentials credentials = super .validate ();
134+ final OAuthTokens tokens = credentials .getOauth ();
135+ final String accessToken = tokens .getAccessToken ();
136+ final PreferencesReader preferences = new HostPreferences (bookmark );
137+ final String tokenExchangeClientId = preferences .getProperty (S3AssumeRoleProtocol .OAUTH_TOKENEXCHANGE_CLIENT_ID );
138+ if (StringUtils .isEmpty (tokenExchangeClientId )) {
139+ log .warn ("Found {} empty, although {} is set to {} - misconfiguration?" , S3AssumeRoleProtocol .OAUTH_TOKENEXCHANGE_CLIENT_ID , OAUTH_TOKENEXCHANGE , preferences .getBoolean (OAUTH_TOKENEXCHANGE ));
140+ return credentials ;
141+ }
142+ try {
143+ final DecodedJWT jwt = JWT .decode (accessToken );
144+
145+ final List <String > auds = jwt .getAudience ();
146+ final String azp = jwt .getClaim (OIDC_AUTHORIZED_PARTY ).asString ();
147+
148+ final boolean audNotUnique = 1 != auds .size (); // either multiple audiences or none
149+ // do exchange if aud is not unique or azp is not equal to aud
150+ if (audNotUnique || !auds .get (0 ).equals (azp )) {
151+ log .debug ("None or multiple audiences found {} or audience differs from azp {}, triggering token-exchange." , Arrays .toString (auds .toArray ()), azp );
152+ return credentials .withOauth (this .exchange (tokens ));
153+ }
154+ }
155+ catch (JWTDecodeException e ) {
156+ throw new LoginFailureException ("Invalid JWT or JSON format in authentication token" , e );
157+ }
158+ return credentials ;
159+ }
116160}
0 commit comments