diff --git a/app/src/net/openid/appauthdemo/IdentityProvider.java b/app/src/net/openid/appauthdemo/IdentityProvider.java index 635bdd34..92e99358 100644 --- a/app/src/net/openid/appauthdemo/IdentityProvider.java +++ b/app/src/net/openid/appauthdemo/IdentityProvider.java @@ -110,7 +110,6 @@ public static List getEnabledProviders(Context context) { private Uri mTokenEndpoint; private Uri mRegistrationEndpoint; private String mClientId; - private String mClientSecret; private Uri mRedirectUri; private String mScope; @@ -215,19 +214,11 @@ public String getClientId() { return mClientId; } - @Nullable - public String getClientSecret() { - return mClientSecret; - } public void setClientId(String clientId) { mClientId = clientId; } - public void setClientSecret(String clientSecret) { - mClientSecret = clientSecret; - } - @NonNull public Uri getRedirectUri() { checkConfigurationRead(); diff --git a/app/src/net/openid/appauthdemo/MainActivity.java b/app/src/net/openid/appauthdemo/MainActivity.java index 9f266d33..3a768ca9 100644 --- a/app/src/net/openid/appauthdemo/MainActivity.java +++ b/app/src/net/openid/appauthdemo/MainActivity.java @@ -29,6 +29,7 @@ import android.widget.LinearLayout; import android.widget.TextView; +import net.openid.appauth.AuthState; import net.openid.appauth.AuthorizationException; import net.openid.appauth.AuthorizationRequest; import net.openid.appauth.AuthorizationService; @@ -85,7 +86,7 @@ public void onFetchConfigurationCompleted( // Do dynamic client registration if no client_id makeRegistrationRequest(serviceConfiguration, idp); } else { - makeAuthRequest(serviceConfiguration, idp); + makeAuthRequest(serviceConfiguration, idp, new AuthState()); } } } @@ -127,7 +128,8 @@ protected void onDestroy() { private void makeAuthRequest( @NonNull AuthorizationServiceConfiguration serviceConfig, - @NonNull IdentityProvider idp) { + @NonNull IdentityProvider idp, + @NonNull AuthState authState) { AuthorizationRequest authRequest = new AuthorizationRequest.Builder( serviceConfig, @@ -144,7 +146,7 @@ private void makeAuthRequest( this, authRequest, serviceConfig.discoveryDoc, - idp.getClientSecret()), + authState), mAuthService.createCustomTabsIntentBuilder() .setToolbarColor(getColorCompat(R.color.colorAccent)) .build()); @@ -171,10 +173,10 @@ public void onRegistrationRequestCompleted( Log.d(TAG, "Registration request complete"); if (registrationResponse != null) { idp.setClientId(registrationResponse.clientId); - idp.setClientSecret(registrationResponse.clientSecret); Log.d(TAG, "Registration request complete successfully"); // Continue with the authentication - makeAuthRequest(registrationResponse.request.configuration, idp); + makeAuthRequest(registrationResponse.request.configuration, idp, + new AuthState((registrationResponse))); } } }); diff --git a/app/src/net/openid/appauthdemo/TokenActivity.java b/app/src/net/openid/appauthdemo/TokenActivity.java index 977be13f..93f50362 100644 --- a/app/src/net/openid/appauthdemo/TokenActivity.java +++ b/app/src/net/openid/appauthdemo/TokenActivity.java @@ -47,8 +47,6 @@ import net.openid.appauth.AuthorizationService; import net.openid.appauth.AuthorizationServiceDiscovery; import net.openid.appauth.ClientAuthentication; -import net.openid.appauth.ClientSecretBasic; -import net.openid.appauth.NoClientAuthentication; import net.openid.appauth.TokenRequest; import net.openid.appauth.TokenResponse; @@ -75,7 +73,7 @@ public class TokenActivity extends AppCompatActivity { private static final String KEY_USER_INFO = "userInfo"; private static final String EXTRA_AUTH_SERVICE_DISCOVERY = "authServiceDiscovery"; - private static final String EXTRA_CLIENT_SECRET = "clientSecret"; + private static final String EXTRA_AUTH_STATE = "authState"; private static final int BUFFER_SIZE = 1024; @@ -110,19 +108,15 @@ protected void onCreate(Bundle savedInstanceState) { } if (mAuthState == null) { + mAuthState = getAuthStateFromIntent(getIntent()); AuthorizationResponse response = AuthorizationResponse.fromIntent(getIntent()); AuthorizationException ex = AuthorizationException.fromIntent(getIntent()); - mAuthState = new AuthState(response, ex); + mAuthState.update(response, ex); if (response != null) { Log.d(TAG, "Received AuthorizationResponse."); showSnackbar(R.string.exchange_notification); - String clientSecret = getClientSecretFromIntent(getIntent()); - if (clientSecret != null) { - exchangeAuthorizationCode(response, new ClientSecretBasic(clientSecret)); - } else { - exchangeAuthorizationCode(response); - } + exchangeAuthorizationCode(response); } else { Log.i(TAG, "Authorization failed: " + ex); showSnackbar(R.string.authorization_failed); @@ -261,19 +255,23 @@ private void refreshAccessToken() { performTokenRequest(mAuthState.createTokenRefreshRequest()); } - private void exchangeAuthorizationCode(AuthorizationResponse authorizationResponse, - ClientAuthentication clientAuth) { - performTokenRequest(authorizationResponse.createTokenExchangeRequest(), clientAuth); - } - private void exchangeAuthorizationCode(AuthorizationResponse authorizationResponse) { performTokenRequest(authorizationResponse.createTokenExchangeRequest()); } - private void performTokenRequest(TokenRequest request, ClientAuthentication clientAuth) { + private void performTokenRequest(TokenRequest request) { + ClientAuthentication clientAuthentication = null; + try { + clientAuthentication = mAuthState.getClientAuthentication(); + } catch (ClientAuthentication.UnsupportedAuthenticationMethod ex) { + Log.d(TAG, "Token request cannot be made, client authentication for the token " + + "endpoint could not be constructed (%s)", ex); + return; + } + mAuthService.performTokenRequest( request, - clientAuth, + clientAuthentication, new AuthorizationService.TokenResponseCallback() { @Override public void onTokenRequestCompleted( @@ -284,10 +282,6 @@ public void onTokenRequestCompleted( }); } - private void performTokenRequest(TokenRequest request) { - performTokenRequest(request, NoClientAuthentication.INSTANCE); - } - private void fetchUserInfo() { if (mAuthState.getAuthorizationServiceConfiguration() == null) { Log.e(TAG, "Cannot make userInfo request without service configuration"); @@ -372,14 +366,12 @@ static PendingIntent createPostAuthorizationIntent( @NonNull Context context, @NonNull AuthorizationRequest request, @Nullable AuthorizationServiceDiscovery discoveryDoc, - @Nullable String clientSecret) { + @NonNull AuthState authState) { Intent intent = new Intent(context, TokenActivity.class); + intent.putExtra(EXTRA_AUTH_STATE, authState.jsonSerializeString()); if (discoveryDoc != null) { intent.putExtra(EXTRA_AUTH_SERVICE_DISCOVERY, discoveryDoc.docJson.toString()); } - if (clientSecret != null) { - intent.putExtra(EXTRA_CLIENT_SECRET, clientSecret); - } return PendingIntent.getActivity(context, request.hashCode(), intent, 0); } @@ -396,11 +388,16 @@ static AuthorizationServiceDiscovery getDiscoveryDocFromIntent(Intent intent) { } } - static String getClientSecretFromIntent(Intent intent) { - if (!intent.hasExtra(EXTRA_CLIENT_SECRET)) { - return null; + static AuthState getAuthStateFromIntent(Intent intent) { + if (!intent.hasExtra(EXTRA_AUTH_STATE)) { + throw new IllegalArgumentException("The AuthState instance is missing in the intent."); + } + try { + return AuthState.jsonDeserialize(intent.getStringExtra(EXTRA_AUTH_STATE)); + } catch (JSONException ex) { + Log.e(TAG, "Malformed AuthState JSON saved", ex); + throw new IllegalArgumentException("The AuthState instance is missing in the intent."); } - return intent.getStringExtra(EXTRA_CLIENT_SECRET); } private class UserProfilePictureTarget implements Target { diff --git a/library/java/net/openid/appauth/AuthState.java b/library/java/net/openid/appauth/AuthState.java index 10022be3..396091bb 100644 --- a/library/java/net/openid/appauth/AuthState.java +++ b/library/java/net/openid/appauth/AuthState.java @@ -50,6 +50,7 @@ public class AuthState { private static final String KEY_LAST_AUTHORIZATION_RESPONSE = "lastAuthorizationResponse"; private static final String KEY_LAST_TOKEN_RESPONSE = "mLastTokenResponse"; private static final String KEY_AUTHORIZATION_EXCEPTION = "mAuthorizationException"; + private static final String KEY_LAST_REGISTRATION_RESPONSE = "lastRegistrationResponse"; @Nullable private String mRefreshToken; @@ -63,6 +64,9 @@ public class AuthState { @Nullable private TokenResponse mLastTokenResponse; + @Nullable + private RegistrationResponse mLastRegistrationResponse; + @Nullable private AuthorizationException mAuthorizationException; @@ -83,6 +87,13 @@ public AuthState(@Nullable AuthorizationResponse authResponse, update(authResponse, authError); } + /** + * Creates an {@link AuthState} based on a dynamic registration client registration request. + */ + public AuthState(@NonNull RegistrationResponse regResponse) { + update(regResponse); + } + /** * Creates an {@link AuthState} based on an authorization exchange and subsequent token * exchange. @@ -151,6 +162,20 @@ public TokenResponse getLastTokenResponse() { return mLastTokenResponse; } + /** + * The most recent client registration response used to update this authorization state. + * + *

+ * It is rarely necessary to directly use the response; instead convenience methods are provided + * to retrieve the {@link #getClientSecret() client secret} and + * {@link #getClientSecretExpirationTime() client secret expiration}. + *

+ */ + @Nullable + public RegistrationResponse getLastRegistrationResponse() { + return mLastRegistrationResponse; + } + /** * The configuration of the authorization service associated with this authorization state. */ @@ -226,6 +251,31 @@ public String getIdToken() { return null; } + /** + * The current client secret, if available. + */ + public String getClientSecret() { + if (mLastRegistrationResponse != null) { + return mLastRegistrationResponse.clientSecret; + } + + return null; + } + + /** + * The expiration time of the current client credentials (if available), as milliseconds from + * the UNIX epoch (consistent with {@link System#currentTimeMillis()}). If the value is 0, the + * client credentials will not expire. + */ + @Nullable + public Long getClientSecretExpirationTime() { + if (mLastRegistrationResponse != null) { + return mLastRegistrationResponse.clientSecretExpiresAt; + } + + return null; + } + /** * Determines whether the current state represents a successful authorization, * from which at least either an access token or an ID token have been retrieved. @@ -282,6 +332,24 @@ public void setNeedsTokenRefresh(boolean needsTokenRefresh) { mNeedsTokenRefreshOverride = needsTokenRefresh; } + /** + * Determines whether the client credentials is considered to have expired. If no client + * credentials have been acquired, then this method will always return {@code false} + */ + public boolean hasClientSecretExpired() { + return hasClientSecretExpired(SystemClock.INSTANCE); + } + + @VisibleForTesting + boolean hasClientSecretExpired(Clock clock) { + if (getClientSecretExpirationTime() == null || getClientSecretExpirationTime() == 0) { + // no explicit expiration time, and 0 means it will not expire + return false; + } + + return getClientSecretExpirationTime() <= clock.getCurrentTimeMillis(); + } + /** * Updates the authorization state based on a new authorization response. */ @@ -346,6 +414,19 @@ public void update( } } + /** + * Updates the authorization state based on a new client registration response. + */ + public void update(@Nullable RegistrationResponse regResponse) { + mLastRegistrationResponse = regResponse; + /* a new client registration will have a new client id, so invalidate the current session */ + mRefreshToken = null; + mScope = null; + mLastAuthorizationResponse = null; + mLastTokenResponse = null; + mAuthorizationException = null; + } + /** * Ensures that a non-expired access token is available before invoking the provided action. */ @@ -469,6 +550,12 @@ public JSONObject jsonSerialize() { KEY_LAST_TOKEN_RESPONSE, mLastTokenResponse.jsonSerialize()); } + if (mLastRegistrationResponse != null) { + JsonUtil.put( + json, + KEY_LAST_REGISTRATION_RESPONSE, + mLastRegistrationResponse.jsonSerialize()); + } return json; } @@ -504,6 +591,10 @@ public static AuthState jsonDeserialize(@NonNull JSONObject json) throws JSONExc state.mLastTokenResponse = TokenResponse.jsonDeserialize( json.getJSONObject(KEY_LAST_TOKEN_RESPONSE)); } + if (json.has(KEY_LAST_REGISTRATION_RESPONSE)) { + state.mLastRegistrationResponse = RegistrationResponse.jsonDeserialize( + json.getJSONObject(KEY_LAST_REGISTRATION_RESPONSE)); + } return state; } @@ -535,4 +626,36 @@ void execute( @Nullable String idToken, @Nullable AuthorizationException ex); } + /** + * Creates the required client authentication for the token endpoint based on information + * in the most recent registration response (if it is set). + * + * @throws ClientAuthentication.UnsupportedAuthenticationMethod if the expected client + * authentication method is unsupported by this client library. + */ + public ClientAuthentication getClientAuthentication() throws + ClientAuthentication.UnsupportedAuthenticationMethod { + if (getClientSecret() == null) { + /* Without client credentials, or unspecified 'token_endpoint_auth_method', + * we can never authenticate */ + return NoClientAuthentication.INSTANCE; + } else if (mLastRegistrationResponse.tokenEndpointAuthMethod == null) { + /* 'token_endpoint_auth_method': "If omitted, the default is client_secret_basic", + * "OpenID Connect Dynamic Client Registration 1.0", Section 2 */ + return new ClientSecretBasic(getClientSecret()); + } + + switch (mLastRegistrationResponse.tokenEndpointAuthMethod) { + case ClientSecretBasic.NAME: + return new ClientSecretBasic(getClientSecret()); + case ClientSecretPost.NAME: + return new ClientSecretPost(getClientSecret()); + case "none": + return NoClientAuthentication.INSTANCE; + default: + throw new ClientAuthentication.UnsupportedAuthenticationMethod( + mLastRegistrationResponse.tokenEndpointAuthMethod); + + } + } } diff --git a/library/java/net/openid/appauth/ClientAuthentication.java b/library/java/net/openid/appauth/ClientAuthentication.java index 921a5b97..4ecad79e 100644 --- a/library/java/net/openid/appauth/ClientAuthentication.java +++ b/library/java/net/openid/appauth/ClientAuthentication.java @@ -17,6 +17,25 @@ import java.util.Map; public interface ClientAuthentication { + /** + * Thrown when a mandatory property is missing from the registration response. + */ + class UnsupportedAuthenticationMethod extends Exception { + private String mAuthMethod; + + /** + * Indicates that the specified client authentication method is unsupported. + */ + public UnsupportedAuthenticationMethod(String field) { + super("Unsupported client authentication method: " + field); + mAuthMethod = field; + } + + public String getUnsupportedAuthenticationMethod() { + return mAuthMethod; + } + } + /** * Constructs any extra parameters necessary to include in the request headers for the client * authentication. diff --git a/library/java/net/openid/appauth/RegistrationResponse.java b/library/java/net/openid/appauth/RegistrationResponse.java index b86ad91b..18a16f50 100644 --- a/library/java/net/openid/appauth/RegistrationResponse.java +++ b/library/java/net/openid/appauth/RegistrationResponse.java @@ -41,6 +41,7 @@ public class RegistrationResponse { static final String PARAM_REGISTRATION_ACCESS_TOKEN = "registration_access_token"; static final String PARAM_REGISTRATION_CLIENT_URI = "registration_client_uri"; static final String PARAM_CLIENT_ID_ISSUED_AT = "client_id_issued_at"; + static final String PARAM_TOKEN_ENDPOINT_AUTH_METHOD = "token_endpoint_auth_method"; static final String KEY_REQUEST = "request"; static final String KEY_ADDITIONAL_PARAMETERS = "additionalParameters"; @@ -51,7 +52,8 @@ public class RegistrationResponse { PARAM_CLIENT_SECRET_EXPIRES_AT, PARAM_REGISTRATION_ACCESS_TOKEN, PARAM_REGISTRATION_CLIENT_URI, - PARAM_CLIENT_ID_ISSUED_AT + PARAM_CLIENT_ID_ISSUED_AT, + PARAM_TOKEN_ENDPOINT_AUTH_METHOD )); /** * The registration request associated with this response. @@ -117,13 +119,21 @@ public class RegistrationResponse { @Nullable public final Uri registrationClientUri; + /** + * Client authentication method to use at the token endpoint, if provided. + * + * @see + * "OpenID Connect Core 1.0", Section 9 + */ + @Nullable + public final String tokenEndpointAuthMethod; + /** * Additional, non-standard parameters in the response. */ @NonNull public final Map additionalParameters; - /** * Thrown when a mandatory property is missing from the registration response. */ @@ -159,6 +169,8 @@ public static final class Builder { private String mRegistrationAccessToken; @Nullable private Uri mRegistrationClientUri; + @Nullable + private String mTokenEndpointAuthMethod; @NonNull private Map mAdditionalParameters = Collections.emptyMap(); @@ -238,6 +250,14 @@ public Builder setRegistrationAccessToken(@Nullable String registrationAccessTok return this; } + /** + * Specifies the client authentication method to use at the token endpoint. + */ + public Builder setTokenEndpointAuthMethod(@Nullable String tokenEndpointAuthMethod) { + mTokenEndpointAuthMethod = tokenEndpointAuthMethod; + return this; + } + /** * Specifies the client configuration endpoint. * @@ -269,6 +289,7 @@ public RegistrationResponse build() { mClientSecretExpiresAt, mRegistrationAccessToken, mRegistrationClientUri, + mTokenEndpointAuthMethod, mAdditionalParameters); } @@ -326,6 +347,8 @@ public Builder fromResponseJson(@NonNull JSONObject json) setRegistrationAccessToken(JsonUtil.getStringIfDefined(json, PARAM_REGISTRATION_ACCESS_TOKEN)); setRegistrationClientUri(JsonUtil.getUriIfDefined(json, PARAM_REGISTRATION_CLIENT_URI)); + setTokenEndpointAuthMethod(JsonUtil.getStringIfDefined(json, + PARAM_TOKEN_ENDPOINT_AUTH_METHOD)); setAdditionalParameters(extractAdditionalParams(json, BUILT_IN_PARAMS)); return this; @@ -340,6 +363,7 @@ private RegistrationResponse( @Nullable Long clientSecretExpiresAt, @Nullable String registrationAccessToken, @Nullable Uri registrationClientUri, + @Nullable String tokenEndpointAuthMethod, @NonNull Map additionalParameters) { this.request = request; this.clientId = clientId; @@ -348,6 +372,7 @@ private RegistrationResponse( this.clientSecretExpiresAt = clientSecretExpiresAt; this.registrationAccessToken = registrationAccessToken; this.registrationClientUri = registrationClientUri; + this.tokenEndpointAuthMethod = tokenEndpointAuthMethod; this.additionalParameters = additionalParameters; } @@ -355,7 +380,7 @@ private RegistrationResponse( * Reads a registration response JSON string received from an authorization server, * and associates it with the provided request. * - * @throws JSONException if the JSON is malformed or missing required fields. + * @throws JSONException if the JSON is malformed or missing required fields. * @throws MissingArgumentException if the JSON is missing fields required by the specification. */ @NonNull @@ -370,7 +395,7 @@ public static RegistrationResponse fromJson( * Reads a registration response JSON object received from an authorization server, * and associates it with the provided request. * - * @throws JSONException if the JSON is malformed or missing required fields. + * @throws JSONException if the JSON is malformed or missing required fields. * @throws MissingArgumentException if the JSON is missing fields required by the specification. */ @NonNull @@ -398,6 +423,7 @@ public JSONObject jsonSerialize() { JsonUtil.putIfNotNull(json, PARAM_CLIENT_SECRET_EXPIRES_AT, clientSecretExpiresAt); JsonUtil.putIfNotNull(json, PARAM_REGISTRATION_ACCESS_TOKEN, registrationAccessToken); JsonUtil.putIfNotNull(json, PARAM_REGISTRATION_CLIENT_URI, registrationClientUri); + JsonUtil.putIfNotNull(json, PARAM_TOKEN_ENDPOINT_AUTH_METHOD, tokenEndpointAuthMethod); JsonUtil.put(json, KEY_ADDITIONAL_PARAMETERS, JsonUtil.mapToJsonObject(additionalParameters)); return json; @@ -416,6 +442,7 @@ public String jsonSerializeString() { /** * Reads a registration response from a JSON string representation produced by * {@link #jsonSerialize()}. + * * @throws JSONException if the provided JSON does not match the expected structure. */ public static RegistrationResponse jsonDeserialize(@NonNull JSONObject json) @@ -441,6 +468,7 @@ public static RegistrationResponse jsonDeserialize(@NonNull JSONObject json) * Reads a registration response from a JSON string representation produced by * {@link #jsonSerializeString()}. This method is just a convenience wrapper for * {@link #jsonDeserialize(JSONObject)}, converting the JSON string to its JSON object form. + * * @throws JSONException if the provided JSON does not match the expected structure. */ @NonNull diff --git a/library/javatests/net/openid/appauth/AuthStateTest.java b/library/javatests/net/openid/appauth/AuthStateTest.java index a8f59b57..dc1436f4 100644 --- a/library/javatests/net/openid/appauth/AuthStateTest.java +++ b/library/javatests/net/openid/appauth/AuthStateTest.java @@ -16,6 +16,7 @@ import static net.openid.appauth.TestValues.TEST_ACCESS_TOKEN; import static net.openid.appauth.TestValues.TEST_AUTH_CODE; +import static net.openid.appauth.TestValues.TEST_CLIENT_SECRET; import static net.openid.appauth.TestValues.TEST_ID_TOKEN; import static net.openid.appauth.TestValues.TEST_REFRESH_TOKEN; import static net.openid.appauth.TestValues.getMinimalAuthRequestBuilder; @@ -24,6 +25,8 @@ import static net.openid.appauth.TestValues.getTestAuthRequest; import static net.openid.appauth.TestValues.getTestAuthResponse; import static net.openid.appauth.TestValues.getTestAuthResponseBuilder; +import static net.openid.appauth.TestValues.getTestRegistrationResponse; +import static net.openid.appauth.TestValues.getTestRegistrationResponseBuilder; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.isNull; @@ -67,6 +70,7 @@ public void testInitialState() { assertThat(state.getLastAuthorizationResponse()).isNull(); assertThat(state.getLastTokenResponse()).isNull(); + assertThat(state.getLastRegistrationResponse()).isNull(); assertThat(state.getScope()).isNull(); assertThat(state.getScopeSet()).isNull(); @@ -156,6 +160,27 @@ public void testInitialState_fromAuthorizationResponseAndTokenResponse() { assertThat(state.getNeedsTokenRefresh(mClock)).isTrue(); } + @Test + public void testInitialState_fromRegistrationResponse() { + RegistrationResponse regResp = getTestRegistrationResponse(); + AuthState state = new AuthState(regResp); + + assertThat(state.isAuthorized()).isFalse(); + assertThat(state.getAccessToken()).isNull(); + assertThat(state.getAccessTokenExpirationTime()).isNull(); + assertThat(state.getIdToken()).isNull(); + assertThat(state.getRefreshToken()).isNull(); + + assertThat(state.getAuthorizationException()).isNull(); + assertThat(state.getLastAuthorizationResponse()).isNull(); + assertThat(state.getLastTokenResponse()).isNull(); + assertThat(state.getLastRegistrationResponse()).isSameAs(regResp); + + assertThat(state.getScope()).isNull(); + assertThat(state.getScopeSet()).isNull(); + assertThat(state.getNeedsTokenRefresh(mClock)).isFalse(); + } + @Test(expected = IllegalArgumentException.class) public void testConstructor_withAuthResponseAndException() { new AuthState(getTestAuthResponse(), @@ -502,7 +527,9 @@ public void testJsonSerialization() throws Exception { .build(); TokenResponse tokenResp = getTestAuthCodeExchangeResponse(); + RegistrationResponse regResp = getTestRegistrationResponse(); AuthState state = new AuthState(authResp, tokenResp, null); + state.update(regResp); String json = state.jsonSerializeString(); AuthState restoredState = AuthState.jsonDeserialize(json); @@ -517,6 +544,9 @@ public void testJsonSerialization() throws Exception { assertThat(restoredState.getScope()).isEqualTo(state.getScope()); assertThat(restoredState.getNeedsTokenRefresh(mClock)) .isEqualTo(state.getNeedsTokenRefresh(mClock)); + assertThat(restoredState.getClientSecret()).isEqualTo(state.getClientSecret()); + assertThat(restoredState.hasClientSecretExpired(mClock)) + .isEqualTo(state.hasClientSecretExpired(mClock)); } @Test @@ -529,4 +559,92 @@ public void testJsonSerialization_withException() throws Exception { assertThat(restored.getAuthorizationException()) .isEqualTo(state.getAuthorizationException()); } + + @Test + public void testHasClientSecretExpired() { + RegistrationResponse regResp = getTestRegistrationResponseBuilder() + .setClientSecret(TEST_CLIENT_SECRET) + .setClientSecretExpiresAt(TWO_MINUTES) + .build(); + AuthState state = new AuthState(regResp); + + // before the expiration time + mClock.currentTime.set(ONE_SECOND); + assertThat(state.hasClientSecretExpired(mClock)).isFalse(); + + // on client_secret's actual expiration + mClock.currentTime.set(TWO_MINUTES); + assertThat(state.hasClientSecretExpired(mClock)).isTrue(); + + // past client_secrets's actual expiration + mClock.currentTime.set(TWO_MINUTES + ONE_SECOND); + assertThat(state.hasClientSecretExpired(mClock)).isTrue(); + } + + @Test + public void testCreateRequiredClientAuthentication_withoutClientCredentials() throws + Exception { + RegistrationResponse regResp = getTestRegistrationResponseBuilder().build(); + AuthState state = new AuthState(regResp); + assertThat(state.getClientAuthentication()) + .isInstanceOf(NoClientAuthentication.class); + } + + @Test + public void testCreateRequiredClientAuthentication_withoutTokenEndpointAuthMethod() throws + Exception { + RegistrationResponse regResp = getTestRegistrationResponseBuilder() + .setClientSecret(TEST_CLIENT_SECRET) + .build(); + AuthState state = new AuthState(regResp); + assertThat(state.getClientAuthentication()) + .isInstanceOf(ClientSecretBasic.class); + } + + @Test + public void testCreateRequiredClientAuthentication_withTokenEndpointAuthMethodNone() throws + Exception { + RegistrationResponse regResp = getTestRegistrationResponseBuilder() + .setClientSecret(TEST_CLIENT_SECRET) + .setTokenEndpointAuthMethod("none") + .build(); + AuthState state = new AuthState(regResp); + assertThat(state.getClientAuthentication()) + .isInstanceOf(NoClientAuthentication.class); + } + + @Test + public void testCreateRequiredClientAuthentication_withTokenEndpointAuthMethodBasic() throws + Exception { + RegistrationResponse regResp = getTestRegistrationResponseBuilder() + .setClientSecret(TEST_CLIENT_SECRET) + .setTokenEndpointAuthMethod(ClientSecretBasic.NAME) + .build(); + AuthState state = new AuthState(regResp); + assertThat(state.getClientAuthentication()) + .isInstanceOf(ClientSecretBasic.class); + } + + @Test + public void testCreateRequiredClientAuthentication_withTokenEndpointAuthMethodPost() throws + Exception { + RegistrationResponse regResp = getTestRegistrationResponseBuilder() + .setClientSecret(TEST_CLIENT_SECRET) + .setTokenEndpointAuthMethod(ClientSecretPost.NAME) + .build(); + AuthState state = new AuthState(regResp); + assertThat(state.getClientAuthentication()) + .isInstanceOf(ClientSecretPost.class); + } + + @Test(expected = ClientAuthentication.UnsupportedAuthenticationMethod.class) + public void testCreateRequiredClientAuthentication_withUnknownTokenEndpointAuthMethod() + throws Exception { + RegistrationResponse regResp = getTestRegistrationResponseBuilder() + .setClientSecret(TEST_CLIENT_SECRET) + .setTokenEndpointAuthMethod("unknown") + .build(); + AuthState state = new AuthState(regResp); + state.getClientAuthentication(); + } } diff --git a/library/javatests/net/openid/appauth/RegistrationResponseTest.java b/library/javatests/net/openid/appauth/RegistrationResponseTest.java index ef2d5955..d825a57a 100644 --- a/library/javatests/net/openid/appauth/RegistrationResponseTest.java +++ b/library/javatests/net/openid/appauth/RegistrationResponseTest.java @@ -47,7 +47,7 @@ public class RegistrationResponseTest { private static final String TEST_REGISTRATION_ACCESS_TOKEN = "test_access_token"; private static final String TEST_REGISTRATION_CLIENT_URI = "https://test.openid.com/register?client_id=" + TEST_CLIENT_ID; - + private static final String TEST_TOKEN_ENDPOINT_AUTH_METHOD = "client_secret_basic"; private static final String TEST_JSON = "{\n" + " \"client_id\": \"" + TEST_CLIENT_ID + "\",\n" @@ -56,7 +56,8 @@ public class RegistrationResponseTest { + " \"client_secret_expires_at\": \"" + TEST_CLIENT_SECRET_EXPIRES_AT + "\",\n" + " \"registration_access_token\": \"" + TEST_REGISTRATION_ACCESS_TOKEN + "\",\n" + " \"registration_client_uri\": \"" + TEST_REGISTRATION_CLIENT_URI + "\",\n" - + " \"application_type\": " + RegistrationRequest.APPLICATION_TYPE_NATIVE + "\n" + + " \"application_type\": \"" + RegistrationRequest.APPLICATION_TYPE_NATIVE + "\",\n" + + " \"token_endpoint_auth_method\": \"" + TEST_TOKEN_ENDPOINT_AUTH_METHOD + "\"\n" + "}"; @RunWith(RobolectricTestRunner.class) @@ -101,6 +102,8 @@ public void testSerialize() throws Exception { .isEqualTo(TEST_REGISTRATION_ACCESS_TOKEN); assertThat(JsonUtil.getUri(json, RegistrationResponse.PARAM_REGISTRATION_CLIENT_URI)) .isEqualTo(Uri.parse(TEST_REGISTRATION_CLIENT_URI)); + assertThat(json.getString(RegistrationResponse.PARAM_TOKEN_ENDPOINT_AUTH_METHOD)) + .isEqualTo(TEST_TOKEN_ENDPOINT_AUTH_METHOD); } @Test @@ -151,6 +154,7 @@ private void assertValues(RegistrationResponse response) { assertThat(response.registrationAccessToken).isEqualTo(TEST_REGISTRATION_ACCESS_TOKEN); assertThat(response.registrationClientUri) .isEqualTo(Uri.parse(TEST_REGISTRATION_CLIENT_URI)); + assertThat(response.tokenEndpointAuthMethod).isEqualTo(TEST_TOKEN_ENDPOINT_AUTH_METHOD); assertThat(response.additionalParameters) .containsEntry(RegistrationRequest.PARAM_APPLICATION_TYPE, RegistrationRequest.APPLICATION_TYPE_NATIVE); diff --git a/library/javatests/net/openid/appauth/TestValues.java b/library/javatests/net/openid/appauth/TestValues.java index 981b84c5..171fd231 100644 --- a/library/javatests/net/openid/appauth/TestValues.java +++ b/library/javatests/net/openid/appauth/TestValues.java @@ -115,4 +115,16 @@ public static RegistrationRequest.Builder getTestRegistrationRequestBuilder() { public static RegistrationRequest getTestRegistrationRequest() { return getTestRegistrationRequestBuilder().build(); } + + public static RegistrationResponse.Builder getTestRegistrationResponseBuilder() { + return new RegistrationResponse.Builder(getTestRegistrationRequest()) + .setClientId(TEST_CLIENT_ID); + } + + public static RegistrationResponse getTestRegistrationResponse() { + return getTestRegistrationResponseBuilder() + .setClientSecret(TEST_CLIENT_SECRET) + .setClientSecretExpiresAt(TEST_CLIENT_SECRET_EXPIRES_AT) + .build(); + } }