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
1 change: 1 addition & 0 deletions ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSString *, NSString *> *)prepareAuthorizationRequestParameters:(NSDictionary<NSString *,NSString *> *)parameters forConnection:(OCConnection *)connection options:(nullable OCAuthenticationMethodBookmarkAuthenticationDataGenerationOptions)options;
- (NSDictionary<NSString *, NSString *> *)tokenRefreshParametersForRefreshToken:(NSString *)refreshToken connection:(OCConnection *)connection;
Expand Down
103 changes: 103 additions & 0 deletions ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -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<NSString *, id> *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<NSString *, NSString *> *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<OCHTTPRequest *> *)detectionRequestsForConnection:(OCConnection *)connection options:(nullable OCAuthenticationMethodDetectionOptions)options
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
121 changes: 120 additions & 1 deletion ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.m
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"
}
}];
}
Expand Down Expand Up @@ -782,13 +788,126 @@ - (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<NSString *, id> *authSecret = [self cachedAuthenticationSecretForConnection:connection];
NSString *idToken = [authSecret valueForKeyPath:@"tokenResponse.id_token"];

// Build logout URL with parameters
NSURLComponents *logoutURLComponents = [NSURLComponents componentsWithString:endSessionEndpointURLString];
NSMutableArray<NSURLQueryItem *> *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";

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";