diff --git a/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.h b/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.h index 3241d96b..b61bf805 100644 --- a/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.h +++ b/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.h @@ -44,6 +44,7 @@ typedef NS_ENUM(NSInteger, OCAuthenticationOAuth2TokenRequestType) #pragma mark - Subclassing points - (nullable NSURL *)authorizationEndpointURLForConnection:(OCConnection *)connection options:(OCAuthenticationMethodDetectionOptions)options; - (nullable NSURL *)tokenEndpointURLForConnection:(OCConnection *)connection options:(OCAuthenticationMethodDetectionOptions)options; +- (nullable NSURL *)revocationEndpointURLForConnection:(OCConnection *)connection options:(OCAuthenticationMethodDetectionOptions)options; - (NSString *)redirectURIForConnection:(OCConnection *)connection; - (NSDictionary *)prepareAuthorizationRequestParameters:(NSDictionary *)parameters forConnection:(OCConnection *)connection options:(nullable OCAuthenticationMethodBookmarkAuthenticationDataGenerationOptions)options; - (NSDictionary *)tokenRefreshParametersForRefreshToken:(NSString *)refreshToken connection:(OCConnection *)connection; diff --git a/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m b/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m index a252e7a9..699a2c3b 100644 --- a/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m +++ b/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m @@ -217,6 +217,13 @@ - (NSURL *)tokenEndpointURLForConnection:(OCConnection *)connection options:(OCA return ([self.class tokenEndpointURLForConnection:connection options:options]); } +- (NSURL *)revocationEndpointURLForConnection:(OCConnection *)connection options:(OCAuthenticationMethodDetectionOptions)options +{ + // OAuth2 does not have a standard revocation endpoint in its basic configuration + // Subclasses can override this to provide revocation endpoint if available + return (nil); +} + - (NSString *)redirectURIForConnection:(OCConnection *)connection { return ([self classSettingForOCClassSettingsKey:OCAuthenticationMethodOAuth2RedirectURI]); @@ -291,6 +298,102 @@ - (NSString *)clientSecret return (nil); } +- (void)deauthenticateConnection:(OCConnection *)connection withCompletionHandler:(OCAuthenticationMethodAuthenticationCompletionHandler)completionHandler +{ + // OAuth2 token revocation implementation (RFC 7009) + NSURL *revocationEndpointURL = [self revocationEndpointURLForConnection:connection options:nil]; + + if (revocationEndpointURL != nil) + { + // Get current tokens + NSDictionary *authSecret = [self cachedAuthenticationSecretForConnection:connection]; + NSString *accessToken = [authSecret valueForKeyPath:OA2AccessToken]; + NSString *refreshToken = [authSecret valueForKeyPath:OA2RefreshToken]; + + if (refreshToken != nil || accessToken != nil) + { + // Prefer revoking refresh token as it will also invalidate access tokens + NSString *tokenToRevoke = refreshToken ?: accessToken; + NSString *tokenTypeHint = refreshToken ? @"refresh_token" : @"access_token"; + + OCLogDebug(@"Revoking OAuth2 token at %@", revocationEndpointURL); + + // Create revocation request + OCHTTPRequest *revocationRequest = [OCHTTPRequest requestWithURL:revocationEndpointURL]; + revocationRequest.method = OCHTTPMethodPOST; + revocationRequest.requiredSignals = connection.authSignals; + + // Set up request parameters + NSDictionary *parameters = @{ + @"token" : tokenToRevoke, + @"token_type_hint" : tokenTypeHint + }; + + [revocationRequest addParameters:parameters]; + + // Add client credentials if needed + NSString *clientID = self.clientID; + NSString *clientSecret = self.clientSecret; + + if (clientID != nil && clientSecret != nil) + { + if ([self sendClientIDAndSecretInPOSTBody]) + { + // Add to POST body + [revocationRequest addParameters:@{ + @"client_id" : clientID, + @"client_secret" : clientSecret + }]; + } + else + { + // Add as Authorization header + [revocationRequest setValue:[OCAuthenticationMethod basicAuthorizationValueForUsername:clientID passphrase:clientSecret] forHeaderField:OCHTTPHeaderFieldNameAuthorization]; + } + } + + // Send revocation request + [connection sendRequest:revocationRequest ephermalCompletionHandler:^(OCHTTPRequest *request, OCHTTPResponse *response, NSError *error) { + if (error != nil) + { + OCLogError(@"Token revocation failed with error: %@", error); + } + else if (response.status.code == OCHTTPStatusCodeOK) + { + OCLogDebug(@"Token revocation successful"); + } + else + { + OCLogWarning(@"Token revocation returned status %ld", (long)response.status.code); + } + + // Clear cached authentication data regardless of revocation result + [self flushCachedAuthenticationSecret]; + + // Call completion handler + if (completionHandler != nil) + { + completionHandler(nil, nil); + } + }]; + + return; + } + } + + // No revocation endpoint or no tokens to revoke + OCLogDebug(@"OAuth2 deauthentication - no revocation endpoint available or no tokens to revoke"); + + // Clear cached authentication data + [self flushCachedAuthenticationSecret]; + + // Call completion handler to indicate deauthentication is complete + if (completionHandler != nil) + { + completionHandler(nil, nil); + } +} + #pragma mark - Authentication Method Detection + (NSArray *)detectionRequestsForConnection:(OCConnection *)connection options:(nullable OCAuthenticationMethodDetectionOptions)options { diff --git a/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.h b/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.h index f3687125..a00f400f 100644 --- a/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.h +++ b/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.h @@ -33,6 +33,7 @@ extern OCAuthenticationMethodIdentifier OCAuthenticationMethodIdentifierOpenIDCo extern OCClassSettingsKey OCAuthenticationMethodOpenIDConnectRedirectURI; extern OCClassSettingsKey OCAuthenticationMethodOpenIDConnectScope; extern OCClassSettingsKey OCAuthenticationMethodOpenIDConnectPrompt; +extern OCClassSettingsKey OCAuthenticationMethodOpenIDConnectPostLogoutRedirectURI; extern OCClassSettingsKey OCAuthenticationMethodOpenIDRegisterClient; extern OCClassSettingsKey OCAuthenticationMethodOpenIDRegisterClientNameTemplate; extern OCClassSettingsKey OCAuthenticationMethodOpenIDFallbackOnClientRegistrationFailure; diff --git a/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.m b/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.m index 6b44a071..8ea1b9b8 100644 --- a/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.m +++ b/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.m @@ -55,7 +55,8 @@ + (void)load OCAuthenticationMethodOpenIDConnectPrompt : @"select_account consent", OCAuthenticationMethodOpenIDRegisterClient : @(YES), OCAuthenticationMethodOpenIDRegisterClientNameTemplate : @"ownCloud/{{os.name}} {{app.version}}", - OCAuthenticationMethodOpenIDFallbackOnClientRegistrationFailure : @(YES) + OCAuthenticationMethodOpenIDFallbackOnClientRegistrationFailure : @(YES), + OCAuthenticationMethodOpenIDConnectPostLogoutRedirectURI : @"" } metadata:@{ OCAuthenticationMethodOpenIDConnectRedirectURI : @{ OCClassSettingsMetadataKeyType : OCClassSettingsMetadataTypeString, @@ -86,6 +87,11 @@ + (void)load OCClassSettingsMetadataKeyType : OCClassSettingsMetadataTypeBoolean, OCClassSettingsMetadataKeyDescription : @"If client registration is enabled, but registration fails, controls if the error should be ignored and the default client ID and secret should be used instead.", OCClassSettingsMetadataKeyCategory : @"OIDC" + }, + OCAuthenticationMethodOpenIDConnectPostLogoutRedirectURI : @{ + OCClassSettingsMetadataKeyType : OCClassSettingsMetadataTypeString, + OCClassSettingsMetadataKeyDescription : @"OpenID Connect Post Logout Redirect URI. If not set, defaults to the OpenID Connect Redirect URI.", + OCClassSettingsMetadataKeyCategory : @"OIDC" } }]; } @@ -782,6 +788,118 @@ - (void)generateBookmarkAuthenticationDataWithConnection:(OCConnection *)connect }]; } +#pragma mark - Logout Support +- (NSURL *)revocationEndpointURLForConnection:(OCConnection *)connection options:(OCAuthenticationMethodDetectionOptions)options +{ + // First check if OIDC metadata has a revocation endpoint + NSString *revocationEndpointURLString; + + if ((revocationEndpointURLString = OCTypedCast(_openIDConfig[@"revocation_endpoint"], NSString)) != nil) + { + return ([NSURL URLWithString:revocationEndpointURLString]); + } + + // Fall back to parent implementation + return ([super revocationEndpointURLForConnection:connection options:options]); +} + +- (void)deauthenticateConnection:(OCConnection *)connection withCompletionHandler:(OCAuthenticationMethodAuthenticationCompletionHandler)completionHandler +{ + // Check for OIDC end_session_endpoint + NSString *endSessionEndpointURLString = OCTypedCast(_openIDConfig[@"end_session_endpoint"], NSString); + + if (endSessionEndpointURLString != nil) + { + // Get current ID token for logout hint + NSDictionary *authSecret = [self cachedAuthenticationSecretForConnection:connection]; + NSString *idToken = [authSecret valueForKeyPath:@"tokenResponse.id_token"]; + + // Build logout URL with parameters + NSURLComponents *logoutURLComponents = [NSURLComponents componentsWithString:endSessionEndpointURLString]; + NSMutableArray *queryItems = [NSMutableArray new]; + + // Add id_token_hint if available (recommended by OIDC spec) + if (idToken != nil) + { + [queryItems addObject:[NSURLQueryItem queryItemWithName:@"id_token_hint" value:idToken]]; + } + + // Add post_logout_redirect_uri if configured + NSString *postLogoutRedirectURI = [self classSettingForOCClassSettingsKey:OCAuthenticationMethodOpenIDConnectPostLogoutRedirectURI]; + if ((postLogoutRedirectURI == nil) || (postLogoutRedirectURI.length == 0)) + { + // Use the app's redirect URI as default + postLogoutRedirectURI = [self redirectURIForConnection:connection]; + } + + if (postLogoutRedirectURI != nil) + { + [queryItems addObject:[NSURLQueryItem queryItemWithName:@"post_logout_redirect_uri" value:postLogoutRedirectURI]]; + } + + // Add state parameter for security + NSString *state = [[NSUUID UUID] UUIDString]; + [queryItems addObject:[NSURLQueryItem queryItemWithName:@"state" value:state]]; + + if (queryItems.count > 0) + { + logoutURLComponents.queryItems = queryItems; + } + + NSURL *logoutURL = logoutURLComponents.URL; + + OCLogDebug(@"Performing OIDC logout at %@", logoutURL); + + // Open logout URL in browser (similar to authentication flow) + NSError *error = nil; + id authenticationSession = nil; + + if ([OCAuthenticationMethodOAuth2 startAuthenticationSession:&authenticationSession + forURL:logoutURL + scheme:[self redirectURIForConnection:connection] + options:nil + completionHandler:^(NSURL *callbackURL, NSError *sessionError) { + if (sessionError != nil) + { + OCLogError(@"OIDC logout session failed: %@", sessionError); + } + else + { + OCLogDebug(@"OIDC logout session completed"); + } + + // Clear cached authentication data regardless of logout result + [self flushCachedAuthenticationSecret]; + + // Reset OIDC configuration + self->_openIDConfig = nil; + self->_clientRegistrationResponse = nil; + self->_clientRegistrationEndpointURL = nil; + self->_clientRegistrationExpirationDate = nil; + self->_clientName = nil; + self->_clientID = nil; + self->_clientSecret = nil; + + // Call completion handler + if (completionHandler != nil) + { + completionHandler(nil, nil); + } + }]) + { + // Authentication session started successfully + return; + } + else + { + OCLogError(@"Failed to start OIDC logout session: %@", error); + } + } + + // Fall back to OAuth2 token revocation if no end_session_endpoint + [super deauthenticateConnection:connection withCompletionHandler:completionHandler]; +} + @end OCAuthenticationMethodIdentifier OCAuthenticationMethodIdentifierOpenIDConnect = @"com.owncloud.openid-connect"; @@ -789,6 +907,7 @@ - (void)generateBookmarkAuthenticationDataWithConnection:(OCConnection *)connect OCClassSettingsKey OCAuthenticationMethodOpenIDConnectRedirectURI = @"oidc-redirect-uri"; OCClassSettingsKey OCAuthenticationMethodOpenIDConnectScope = @"oidc-scope"; OCClassSettingsKey OCAuthenticationMethodOpenIDConnectPrompt = @"oidc-prompt"; +OCClassSettingsKey OCAuthenticationMethodOpenIDConnectPostLogoutRedirectURI = @"oidc-post-logout-redirect-uri"; OCClassSettingsKey OCAuthenticationMethodOpenIDRegisterClient = @"oidc-register-client"; OCClassSettingsKey OCAuthenticationMethodOpenIDRegisterClientNameTemplate = @"oidc-register-client-name-template"; OCClassSettingsKey OCAuthenticationMethodOpenIDFallbackOnClientRegistrationFailure = @"oidc-fallback-on-client-registration-failure";