From 038cfd9aadc567ea710de67d7475fe722517b2f8 Mon Sep 17 00:00:00 2001 From: Michael Stingl Date: Tue, 8 Jul 2025 22:04:34 +0200 Subject: [PATCH 1/4] Add OAuth2/OIDC logout support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement deauthenticateConnection method in OCAuthenticationMethodOAuth2 with token revocation (RFC 7009) - Add revocationEndpointURLForConnection method for endpoint discovery - Override deauthenticateConnection in OCAuthenticationMethodOpenIDConnect to use OIDC end_session_endpoint - Support proper OIDC logout flow with id_token_hint and post_logout_redirect_uri - Clear all authentication data and OIDC configuration on logout - Fallback to OAuth2 token revocation when OIDC logout endpoint not available 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../OCAuthenticationMethodOAuth2.h | 1 + .../OCAuthenticationMethodOAuth2.m | 103 ++++++++++++++++ .../OCAuthenticationMethodOpenIDConnect.m | 112 ++++++++++++++++++ 3 files changed, 216 insertions(+) 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..63b8b55a 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.bookmark.certificateStore.requiredSignals; + + // 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.authorizationHeaderValue = [OCAuthenticationMethod basicAuthorizationValueForUsername:clientID passphrase:clientSecret]; + } + } + + // 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); + } + }] startWithRunLoopMode:NSDefaultRunLoopMode]; + + 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.m b/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.m index 6b44a071..e36ce86e 100644 --- a/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.m +++ b/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.m @@ -782,6 +782,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:@"oidc-post-logout-redirect-uri"]; + if (postLogoutRedirectURI == nil) + { + // 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"; From 8c8f092439e10364eeea45193bd60bfe198b0740 Mon Sep 17 00:00:00 2001 From: Michael Stingl Date: Tue, 8 Jul 2025 22:31:24 +0200 Subject: [PATCH 2/4] Add oidc-post-logout-redirect-uri configuration option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add OCAuthenticationMethodOpenIDConnectPostLogoutRedirectURI class setting - Document the setting with proper metadata for auto-documentation - Default to empty string, falls back to redirect URI if not configured - Allows customization of post-logout redirect behavior 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../OCAuthenticationMethodOpenIDConnect.h | 1 + .../OCAuthenticationMethodOpenIDConnect.m | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) 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 e36ce86e..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" } }]; } @@ -819,8 +825,8 @@ - (void)deauthenticateConnection:(OCConnection *)connection withCompletionHandle } // Add post_logout_redirect_uri if configured - NSString *postLogoutRedirectURI = [self classSettingForOCClassSettingsKey:@"oidc-post-logout-redirect-uri"]; - if (postLogoutRedirectURI == nil) + NSString *postLogoutRedirectURI = [self classSettingForOCClassSettingsKey:OCAuthenticationMethodOpenIDConnectPostLogoutRedirectURI]; + if ((postLogoutRedirectURI == nil) || (postLogoutRedirectURI.length == 0)) { // Use the app's redirect URI as default postLogoutRedirectURI = [self redirectURIForConnection:connection]; @@ -901,6 +907,7 @@ - (void)deauthenticateConnection:(OCConnection *)connection withCompletionHandle 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"; From 5aca57c5cc015cbf201a09799d44cf53b31acd10 Mon Sep 17 00:00:00 2001 From: Michael Stingl Date: Tue, 8 Jul 2025 22:35:04 +0200 Subject: [PATCH 3/4] Fix requiredSignals usage in deauthenticate method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use connection.authSignals instead of connection.bookmark.certificateStore.requiredSignals to match the pattern used in other authentication requests. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m b/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m index 63b8b55a..d6cb598b 100644 --- a/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m +++ b/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m @@ -321,7 +321,7 @@ - (void)deauthenticateConnection:(OCConnection *)connection withCompletionHandle // Create revocation request OCHTTPRequest *revocationRequest = [OCHTTPRequest requestWithURL:revocationEndpointURL]; revocationRequest.method = OCHTTPMethodPOST; - revocationRequest.requiredSignals = connection.bookmark.certificateStore.requiredSignals; + revocationRequest.requiredSignals = connection.authSignals; // Set up request parameters NSDictionary *parameters = @{ From 3bb166c5da6117da49de317da79903ff347e465a Mon Sep 17 00:00:00 2001 From: Michael Stingl Date: Tue, 8 Jul 2025 22:45:26 +0200 Subject: [PATCH 4/4] Fix OCHTTPRequest authorization header setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use setValue:forHeaderField: instead of non-existent authorizationHeaderValue property 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m b/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m index d6cb598b..699a2c3b 100644 --- a/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m +++ b/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m @@ -348,12 +348,12 @@ - (void)deauthenticateConnection:(OCConnection *)connection withCompletionHandle else { // Add as Authorization header - revocationRequest.authorizationHeaderValue = [OCAuthenticationMethod basicAuthorizationValueForUsername:clientID passphrase:clientSecret]; + [revocationRequest setValue:[OCAuthenticationMethod basicAuthorizationValueForUsername:clientID passphrase:clientSecret] forHeaderField:OCHTTPHeaderFieldNameAuthorization]; } } // Send revocation request - [[connection sendRequest:revocationRequest ephermalCompletionHandler:^(OCHTTPRequest *request, OCHTTPResponse *response, NSError *error) { + [connection sendRequest:revocationRequest ephermalCompletionHandler:^(OCHTTPRequest *request, OCHTTPResponse *response, NSError *error) { if (error != nil) { OCLogError(@"Token revocation failed with error: %@", error); @@ -375,7 +375,7 @@ - (void)deauthenticateConnection:(OCConnection *)connection withCompletionHandle { completionHandler(nil, nil); } - }] startWithRunLoopMode:NSDefaultRunLoopMode]; + }]; return; }