From 85e09cbc1fa9fbd5ddab961787d98130c0b2f1f2 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Fri, 8 Jan 2021 16:59:45 +0100 Subject: [PATCH] =?UTF-8?q?OAuth2:=20-=20add=20new=20method=20-postProcess?= =?UTF-8?q?AuthenticationDataDict:=20to=20condense=20the=20stored=20token?= =?UTF-8?q?=20response=20to=20the=20needed=20essentials,=20saving=20space?= =?UTF-8?q?=20and=20steering=20clear=20of=20issues=20if=20the=20token=20re?= =?UTF-8?q?sponse=20contains=20null=20values=20that=20make=20secret=20seri?= =?UTF-8?q?alization=20to=20property=20list=20format=20fail=20-=20factor?= =?UTF-8?q?=20out=20access=20to=20clientID=20and=20clientSecret=20into=20m?= =?UTF-8?q?ethods=20-=20make=20-sendTokenRequestToConnection:=E2=80=A6=20s?= =?UTF-8?q?ubclass=20able=20-=20improved=20error=20handling=20if=20propert?= =?UTF-8?q?y=20list=20serialization=20fails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OIDC: - add support for OpenID Connect Dynamic Client Registration - on by default for servers offering the endpoint - including support for expiration, preservation and caching - add additional error code for client registration failure HTTP Pipeline: - factor out User-Agent template composition method to make it available - extend OCHTTPRequest with JSON-specific method to easily instantiate POST requests with JSON payload --- ownCloudSDK.xcodeproj/project.pbxproj | 8 + .../Authentication/OCAuthenticationMethod.m | 2 + .../OCAuthenticationMethodOAuth2.h | 11 + .../OCAuthenticationMethodOAuth2.m | 64 ++- .../OCAuthenticationMethodOpenIDConnect.h | 2 + .../OCAuthenticationMethodOpenIDConnect.m | 386 +++++++++++++++++- ownCloudSDK/Errors/NSError+OCError.h | 4 +- ownCloudSDK/Errors/NSError+OCError.m | 4 + ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.h | 3 + ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.m | 51 ++- ownCloudSDK/HTTP/Request/OCHTTPRequest+JSON.h | 32 ++ ownCloudSDK/HTTP/Request/OCHTTPRequest+JSON.m | 56 +++ .../Resources/en.lproj/Localizable.strings | 3 + ownCloudSDK/ownCloudSDK.h | 1 + 14 files changed, 590 insertions(+), 37 deletions(-) create mode 100644 ownCloudSDK/HTTP/Request/OCHTTPRequest+JSON.h create mode 100644 ownCloudSDK/HTTP/Request/OCHTTPRequest+JSON.m diff --git a/ownCloudSDK.xcodeproj/project.pbxproj b/ownCloudSDK.xcodeproj/project.pbxproj index 44f92725..4c99bce3 100644 --- a/ownCloudSDK.xcodeproj/project.pbxproj +++ b/ownCloudSDK.xcodeproj/project.pbxproj @@ -386,6 +386,8 @@ DC98BDF821E73EFF003B5658 /* Network.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC98BDF721E73EFF003B5658 /* Network.framework */; settings = {ATTRIBUTES = (Required, ); }; }; DC9B4D3922E987EF0089BF78 /* OCClaim.m in Sources */ = {isa = PBXBuildFile; fileRef = DC1D4D3E20DC2281005A3DFC /* OCClaim.m */; }; DC9B4D3A22E987EF0089BF78 /* OCClaim.h in Headers */ = {isa = PBXBuildFile; fileRef = DC1D4D3D20DC2281005A3DFC /* OCClaim.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DC9D22EA25A8754200CF5675 /* OCHTTPRequest+JSON.h in Headers */ = {isa = PBXBuildFile; fileRef = DC9D22E825A8754200CF5675 /* OCHTTPRequest+JSON.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DC9D22EB25A8754200CF5675 /* OCHTTPRequest+JSON.m in Sources */ = {isa = PBXBuildFile; fileRef = DC9D22E925A8754200CF5675 /* OCHTTPRequest+JSON.m */; }; DCA35D4D24CF685B00DBE2B0 /* OCDiagnosticNode.h in Headers */ = {isa = PBXBuildFile; fileRef = DCA35D4B24CF685B00DBE2B0 /* OCDiagnosticNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCA35D4E24CF685B00DBE2B0 /* OCDiagnosticNode.m in Sources */ = {isa = PBXBuildFile; fileRef = DCA35D4C24CF685B00DBE2B0 /* OCDiagnosticNode.m */; }; DCA35D5524CF688700DBE2B0 /* OCDiagnosticSource.h in Headers */ = {isa = PBXBuildFile; fileRef = DCA35D5324CF688700DBE2B0 /* OCDiagnosticSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1134,6 +1136,8 @@ DC98BDF321E73ECE003B5658 /* OCCoreNetworkMonitorSignalProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCCoreNetworkMonitorSignalProvider.h; sourceTree = ""; }; DC98BDF421E73ECE003B5658 /* OCCoreNetworkMonitorSignalProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCCoreNetworkMonitorSignalProvider.m; sourceTree = ""; }; DC98BDF721E73EFF003B5658 /* Network.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Network.framework; path = System/Library/Frameworks/Network.framework; sourceTree = SDKROOT; }; + DC9D22E825A8754200CF5675 /* OCHTTPRequest+JSON.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCHTTPRequest+JSON.h"; sourceTree = ""; }; + DC9D22E925A8754200CF5675 /* OCHTTPRequest+JSON.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCHTTPRequest+JSON.m"; sourceTree = ""; }; DCA35D4B24CF685B00DBE2B0 /* OCDiagnosticNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCDiagnosticNode.h; sourceTree = ""; }; DCA35D4C24CF685B00DBE2B0 /* OCDiagnosticNode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCDiagnosticNode.m; sourceTree = ""; }; DCA35D5324CF688700DBE2B0 /* OCDiagnosticSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCDiagnosticSource.h; sourceTree = ""; }; @@ -2089,6 +2093,8 @@ DC8556EB204DEA2900189B9A /* OCHTTPDAVRequest.h */, DC00DB1C219B120300C82737 /* OCHTTPDAVMultistatusResponse.m */, DC00DB1B219B120300C82737 /* OCHTTPDAVMultistatusResponse.h */, + DC9D22E925A8754200CF5675 /* OCHTTPRequest+JSON.m */, + DC9D22E825A8754200CF5675 /* OCHTTPRequest+JSON.h */, ); path = Request; sourceTree = ""; @@ -3073,6 +3079,7 @@ DCDD9B18222989E50052A001 /* OCRecipient.h in Headers */, DCADC03F2072774200DB8E83 /* OCQuery+Internal.h in Headers */, DC4A2C5E20D4608100A47260 /* OCIssueChoice.h in Headers */, + DC9D22EA25A8754200CF5675 /* OCHTTPRequest+JSON.h in Headers */, DC07C29221244FD800B815A4 /* OCExtension.h in Headers */, DCC8FA25202B259D00EB6701 /* OCSyncRecord.h in Headers */, DC2D646821C3D71000EB26FD /* OCCore+Thumbnails.h in Headers */, @@ -3599,6 +3606,7 @@ DC3CE066242A49E100AB8B88 /* OCMessagePresenter.m in Sources */, DCD038A12542CA4500F97534 /* NSString+OCClassSettings.m in Sources */, DC68057E212EB438006C3B1F /* OCExtensionMatch.m in Sources */, + DC9D22EB25A8754200CF5675 /* OCHTTPRequest+JSON.m in Sources */, DC302AEF221EAC55003218C6 /* OCProxyProgress.m in Sources */, DC4B1172220830F20062BCDD /* OCHTTPPipelineBackend.m in Sources */, DCE26621211348B00001FB2C /* OCCore+CommandLocalImport.m in Sources */, diff --git a/ownCloudSDK/Authentication/OCAuthenticationMethod.m b/ownCloudSDK/Authentication/OCAuthenticationMethod.m index e4b4115c..1dbafdc1 100644 --- a/ownCloudSDK/Authentication/OCAuthenticationMethod.m +++ b/ownCloudSDK/Authentication/OCAuthenticationMethod.m @@ -276,7 +276,9 @@ - (void)flushCachedAuthenticationSecret @synchronized(self) { _cachedAuthenticationSecret = nil; + [self willChangeValueForKey:@"authenticationDataKnownInvalidDate"]; _authenticationDataKnownInvalidDate = nil; + [self didChangeValueForKey:@"authenticationDataKnownInvalidDate"]; } } diff --git a/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.h b/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.h index 74dd09af..2c4b8a8d 100644 --- a/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.h +++ b/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.h @@ -23,6 +23,12 @@ NS_ASSUME_NONNULL_BEGIN +typedef NS_ENUM(NSInteger, OCAuthenticationOAuth2TokenRequestType) +{ + OCAuthenticationOAuth2TokenRequestTypeAuthorizationCode, + OCAuthenticationOAuth2TokenRequestTypeRefreshToken +}; + @interface OCAuthenticationMethodOAuth2 : OCAuthenticationMethod @property(strong,nullable,class,nonatomic) Class browserSessionClass; @@ -38,9 +44,14 @@ NS_ASSUME_NONNULL_BEGIN - (NSString *)redirectURIForConnection:(OCConnection *)connection; - (NSDictionary *)prepareAuthorizationRequestParameters:(NSDictionary *)parameters forConnection:(OCConnection *)connection options:(nullable OCAuthenticationMethodBookmarkAuthenticationDataGenerationOptions)options; - (NSDictionary *)tokenRefreshParametersForRefreshToken:(NSString *)refreshToken; +- (NSDictionary *)postProcessAuthenticationDataDict:(NSDictionary *)authDataDict; - (void)retrieveEndpointInformationForConnection:(OCConnection *)connection completionHandler:(void(^)(NSError *error))completionHandler; - (nullable NSString *)scope; - (nullable NSString *)prompt; +- (NSString *)clientID; +- (NSString *)clientSecret; + +- (void)sendTokenRequestToConnection:(OCConnection *)connection withParameters:(NSDictionary *)parameters requestType:(OCAuthenticationOAuth2TokenRequestType)requestType completionHandler:(void(^)(NSError * _Nullable error, NSDictionary * _Nullable jsonResponseDict, NSData * _Nullable authenticationData))completionHandler; @end diff --git a/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m b/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m index 53042e07..ff98d611 100644 --- a/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m +++ b/ownCloudSDK/Authentication/OCAuthenticationMethodOAuth2.m @@ -36,21 +36,15 @@ static OA2DictKeyPath OA2TokenResponse = @"tokenResponse"; static OA2DictKeyPath OA2BearerString = @"bearerString"; + +// FOR ADDITIONS, ALSO UPDATE -postProcessAuthenticationDataDict: !! static OA2DictKeyPath OA2AccessToken = @"tokenResponse.access_token"; static OA2DictKeyPath OA2RefreshToken = @"tokenResponse.refresh_token"; static OA2DictKeyPath OA2ExpiresInSecs = @"tokenResponse.expires_in"; -static OA2DictKeyPath OA2TokenType = @"tokenResponse.token_type"; -static OA2DictKeyPath OA2MessageURL = @"tokenResponse.message_url"; static OA2DictKeyPath OA2UserID = @"tokenResponse.user_id"; #define OA2RefreshSafetyMarginInSeconds 120 -typedef NS_ENUM(NSInteger, OCAuthenticationOAuth2TokenRequestType) -{ - OCAuthenticationOAuth2TokenRequestTypeAuthorizationCode, - OCAuthenticationOAuth2TokenRequestTypeRefreshToken -}; - #ifndef __IPHONE_13_0 #define __IPHONE_13_0 130000 #endif /* __IPHONE_13_0 */ @@ -206,6 +200,30 @@ - (void)retrieveEndpointInformationForConnection:(OCConnection *)connection comp completionHandler(OCError(OCErrorFeatureNotImplemented)); } +- (NSDictionary *)postProcessAuthenticationDataDict:(NSDictionary *)authDataDict +{ + NSDictionary *tokenResponseDict; + + // JSON *could* contain unneeded/unrelated NSNull.null values, which can't be serialized as NSPropertyList, so limit + // the entries in the response dictionary to what is actually needed + if ((tokenResponseDict = authDataDict[OA2TokenResponse]) != nil) + { + NSMutableDictionary *condensedAuthDataDict = [authDataDict mutableCopy]; + NSMutableDictionary *condensedTokenResponse = [NSMutableDictionary new]; + + condensedTokenResponse[@"access_token"] = tokenResponseDict[@"access_token"]; + condensedTokenResponse[@"refresh_token"] = tokenResponseDict[@"refresh_token"]; + condensedTokenResponse[@"expires_in"] = tokenResponseDict[@"expires_in"]; + condensedTokenResponse[@"user_id"] = tokenResponseDict[@"user_id"]; + + condensedAuthDataDict[OA2TokenResponse] = condensedTokenResponse; + + return (condensedAuthDataDict); + } + + return (authDataDict); +} + - (nullable NSString *)scope { return (nil); @@ -216,6 +234,16 @@ - (nullable NSString *)prompt return (nil); } +- (NSString *)clientID +{ + return ([self classSettingForOCClassSettingsKey:OCAuthenticationMethodOAuth2ClientID]); +} + +- (NSString *)clientSecret +{ + return ([self classSettingForOCClassSettingsKey:OCAuthenticationMethodOAuth2ClientSecret]); +} + #pragma mark - Authentication / Deauthentication ("Login / Logout") - (NSDictionary *)authorizationHeadersForConnection:(OCConnection *)connection error:(NSError **)outError { @@ -320,7 +348,7 @@ - (void)generateBookmarkAuthenticationDataWithConnection:(OCConnection *)connect NSDictionary *parameters = @{ // OAuth2 @"response_type" : @"code", - @"client_id" : [self classSettingForOCClassSettingsKey:OCAuthenticationMethodOAuth2ClientID], + @"client_id" : [self clientID], @"redirect_uri" : [self redirectURIForConnection:connection], // OAuth2 PKCE @@ -352,7 +380,7 @@ - (void)generateBookmarkAuthenticationDataWithConnection:(OCConnection *)connect OCLogDebug(@"Auth session concluded with authorization code: %@", OCLogPrivate(authorizationCode)); // Send Access Token Request - [self _sendTokenRequestToConnection:connection + [self sendTokenRequestToConnection:connection withParameters:@{ // OAuth2 @"grant_type" : @"authorization_code", @@ -601,7 +629,7 @@ - (void)_refreshTokenForConnection:(OCConnection *)connection availabilityHandle { OCLogDebug(@"Sending token refresh request for connection (expiry=%@)..", authSecret[OA2ExpirationDate]); - [self _sendTokenRequestToConnection:connection + [self sendTokenRequestToConnection:connection withParameters:[self tokenRefreshParametersForRefreshToken:refreshToken] requestType:OCAuthenticationOAuth2TokenRequestTypeRefreshToken completionHandler:^(NSError *error, NSDictionary *jsonResponseDict, NSData *authenticationData){ @@ -666,7 +694,7 @@ - (void)_refreshTokenForConnection:(OCConnection *)connection availabilityHandle } } -- (void)_sendTokenRequestToConnection:(OCConnection *)connection withParameters:(NSDictionary *)parameters requestType:(OCAuthenticationOAuth2TokenRequestType)requestType completionHandler:(void(^)(NSError *error, NSDictionary *jsonResponseDict, NSData *authenticationData))completionHandler +- (void)sendTokenRequestToConnection:(OCConnection *)connection withParameters:(NSDictionary *)parameters requestType:(OCAuthenticationOAuth2TokenRequestType)requestType completionHandler:(void(^)(NSError *error, NSDictionary *jsonResponseDict, NSData *authenticationData))completionHandler { OCHTTPRequest *tokenRequest; NSDictionary *previousAuthSecret = (requestType == OCAuthenticationOAuth2TokenRequestTypeRefreshToken) ? [self cachedAuthenticationSecretForConnection:connection] : nil; @@ -696,7 +724,7 @@ - (void)_sendTokenRequestToConnection:(OCConnection *)connection withParameters: [self retrieveEndpointInformationForConnection:connection completionHandler:^(NSError * _Nonnull error) { if (error == nil) { - [self _sendTokenRequestToConnection:connection withParameters:parameters requestType:requestType completionHandler:completionHandler]; + [self sendTokenRequestToConnection:connection withParameters:parameters requestType:requestType completionHandler:completionHandler]; } else { @@ -719,8 +747,7 @@ - (void)_sendTokenRequestToConnection:(OCConnection *)connection withParameters: [tokenRequest addParameters:parameters]; - [tokenRequest setValue:[OCAuthenticationMethod basicAuthorizationValueForUsername:[self classSettingForOCClassSettingsKey:OCAuthenticationMethodOAuth2ClientID] passphrase:[self classSettingForOCClassSettingsKey:OCAuthenticationMethodOAuth2ClientSecret]] - forHeaderField:OCHTTPHeaderFieldNameAuthorization]; + [tokenRequest setValue:[OCAuthenticationMethod basicAuthorizationValueForUsername:[self clientID] passphrase:[self clientSecret]] forHeaderField:OCHTTPHeaderFieldNameAuthorization]; // Send Token Request [connection sendRequest:tokenRequest ephermalCompletionHandler:^(OCHTTPRequest *request, OCHTTPResponse *response, NSError *error) { @@ -805,12 +832,19 @@ - (void)_sendTokenRequestToConnection:(OCConnection *)connection withParameters: OA2TokenResponse : jsonResponseDict }; + // Give opportunity to add additional keys + authenticationDataDict = [self postProcessAuthenticationDataDict:authenticationDataDict]; + OCLogDebug(@"Token authorization succeeded with: %@", OCLogPrivate(authenticationDataDict)); if ((authenticationData = [NSPropertyListSerialization dataWithPropertyList:authenticationDataDict format:NSPropertyListBinaryFormat_v1_0 options:0 error:&error]) != nil) { completionHandler(nil, jsonResponseDict, authenticationData); } + else if (error != nil) + { + completionHandler(OCErrorFromError(OCErrorInternal, error), nil, nil); + } else if (error == nil) { completionHandler(OCError(OCErrorInternal), nil, nil); diff --git a/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.h b/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.h index 8b156e90..d78b61d6 100644 --- a/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.h +++ b/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.h @@ -32,5 +32,7 @@ extern OCAuthenticationMethodIdentifier OCAuthenticationMethodIdentifierOpenIDCo extern OCClassSettingsKey OCAuthenticationMethodOpenIDConnectRedirectURI; extern OCClassSettingsKey OCAuthenticationMethodOpenIDConnectScope; +extern OCClassSettingsKey OCAuthenticationMethodOpenIDRegisterClient; +extern OCClassSettingsKey OCAuthenticationMethodOpenIDRegisterClientNameTemplate; NS_ASSUME_NONNULL_END diff --git a/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.m b/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.m index 8bf0eaf0..df3728d2 100644 --- a/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.m +++ b/ownCloudSDK/Authentication/OCAuthenticationMethodOpenIDConnect.m @@ -21,8 +21,29 @@ #import "OCConnection.h" #import "OCLogger.h" #import "OCMacros.h" +#import "OCHTTPRequest+JSON.h" #import "NSError+OCError.h" +#pragma mark - Internal OA2 keys +typedef NSString* OIDCDictKeyPath; +static OIDCDictKeyPath OIDCKeyPathClientRegistrationResponseSerialized = @"clientRegistrationResponseSerialized"; +static OIDCDictKeyPath OIDCKeyPathClientRegistrationEndpointURL = @"clientRegistrationEndpointURL"; +static OIDCDictKeyPath OIDCKeyPathClientRegistrationExpirationDate = @"clientRegistrationExpirationDate"; +static OIDCDictKeyPath OIDCKeyPathClientID = @"clientRegistrationClientID"; +static OIDCDictKeyPath OIDCKeyPathClientSecret = @"clientRegistrationClientSecret"; + +@interface OCAuthenticationMethodOpenIDConnect () +{ + NSDictionary *_clientRegistrationResponse; // JSON response from client registration + NSURL *_clientRegistrationEndpointURL; // URL the client registration was last performed at + NSDate *_clientRegistrationExpirationDate; // nil if it does not expire, the expiry date if it expires + + NSString *_clientName; + NSString *_clientID; + NSString *_clientSecret; +} +@end + @implementation OCAuthenticationMethodOpenIDConnect #pragma mark - Class settings @@ -33,7 +54,9 @@ + (void)load [self registerOCClassSettingsDefaults:@{ OCAuthenticationMethodOpenIDConnectRedirectURI : @"oc://ios.owncloud.com", - OCAuthenticationMethodOpenIDConnectScope : @"openid offline_access email profile" + OCAuthenticationMethodOpenIDConnectScope : @"openid offline_access email profile", + OCAuthenticationMethodOpenIDRegisterClient : @(YES), + OCAuthenticationMethodOpenIDRegisterClientNameTemplate : @"ownCloud/{{os.name}} {{app.version}}" } metadata:@{ OCAuthenticationMethodOpenIDConnectRedirectURI : @{ OCClassSettingsMetadataKeyType : OCClassSettingsMetadataTypeString, @@ -44,6 +67,16 @@ + (void)load OCClassSettingsMetadataKeyType : OCClassSettingsMetadataTypeString, OCClassSettingsMetadataKeyDescription : @"OpenID Connect Scope", OCClassSettingsMetadataKeyCategory : @"OIDC" + }, + OCAuthenticationMethodOpenIDRegisterClient : @{ + OCClassSettingsMetadataKeyType : OCClassSettingsMetadataTypeBoolean, + OCClassSettingsMetadataKeyDescription : @"Use OpenID Connect Dynamic Client Registration if the `.well-known/openid-configuration` provides a `registration_endpoint`. If this option is enabled and a registration endpoint is available, `oa2-client-id` and `oa2-client-secret` will be ignored.", + OCClassSettingsMetadataKeyCategory : @"OIDC" + }, + OCAuthenticationMethodOpenIDRegisterClientNameTemplate : @{ + OCClassSettingsMetadataKeyType : OCClassSettingsMetadataTypeString, + OCClassSettingsMetadataKeyDescription : @"Client Name Template to use during OpenID Connect Dynamic Client Registration. In addition to the placeholders available for `http.user-agent`, `{{url.hostname}}` can also be used.", + OCClassSettingsMetadataKeyCategory : @"OIDC" } }]; } @@ -98,8 +131,8 @@ - (NSString *)redirectURIForConnection:(OCConnection *)connection { NSMutableDictionary *refreshParameters = [[super tokenRefreshParametersForRefreshToken:refreshToken] mutableCopy]; - refreshParameters[@"client_id"] = [self classSettingForOCClassSettingsKey:OCAuthenticationMethodOAuth2ClientID]; - refreshParameters[@"client_secret"] = [self classSettingForOCClassSettingsKey:OCAuthenticationMethodOAuth2ClientSecret]; + refreshParameters[@"client_id"] = self.clientID; + refreshParameters[@"client_secret"] = self.clientSecret; refreshParameters[@"scope"] = self.scope; return (refreshParameters); @@ -122,6 +155,25 @@ - (void)retrieveEndpointInformationForConnection:(OCConnection *)connection comp { self.pkce = [OCPKCE new]; // Enable PKCE + // Dynamic Client Registration support + if ([[self classSettingForOCClassSettingsKey:OCAuthenticationMethodOpenIDRegisterClient] boolValue]) + { + if (self->_openIDConfig[@"registration_endpoint"] != nil) + { + NSURL *registrationEndpointURL; + + if ((registrationEndpointURL = [NSURL URLWithString:self->_openIDConfig[@"registration_endpoint"]]) != nil) + { + OCTLogDebug(@[@"ClientRegistration"], @"Found OIDC dynamic client registration endpoint: %@", registrationEndpointURL); + + // Perform dynamic client registration + [self registerClientWithRegistrationEndpointURL:registrationEndpointURL connection:connection completionHandler:completionHandler]; + + return; + } + } + } + completionHandler(nil); } else @@ -146,6 +198,318 @@ - (NSString *)prompt return (@"consent"); } +#pragma mark - Dynamic Client Registration +- (void)registerClientWithRegistrationEndpointURL:(NSURL *)registrationEndpointURL connection:(OCConnection *)connection completionHandler:(void(^)(NSError *error))completionHandler +{ + NSError *error = nil; + NSDictionary *jsonRequestDict = nil; + OCHTTPRequest *openidClientRegistrationRequest; + + // Check if we have a valid registration + if ((self.clientID != nil) && (self.clientSecret != nil) && // clientID and clientSecret exist + [self.clientRegistrationEndpointURL isEqual:registrationEndpointURL] && // the registration endpoint hasn't changed + ( (self.clientRegistrationExpirationDate==nil) || // either: a) the combination does not expire + ((self.clientRegistrationExpirationDate != nil) && (self.clientRegistrationExpirationDate.timeIntervalSinceNow > 60)) // or: b) it expires, but the expiry date has not yet been reached (with a safety margin of 60 seconds) + ) + ) + { + OCTLogDebug(@[@"ClientRegistration"], @"Using cached client ID/secret from previous registration (expiring %@): %@/%@", self.clientRegistrationExpirationDate, OCLogPrivate(self.clientID), OCLogPrivate(self.clientSecret)); + completionHandler(nil); + return; + } + + // Generate the client name from the template + self->_clientName = [OCHTTPPipeline stringForTemplate:[self classSettingForOCClassSettingsKey:OCAuthenticationMethodOpenIDRegisterClientNameTemplate] variables:@{ @"url.hostname" : connection.bookmark.url.host }]; + + // Compose JSON request + jsonRequestDict = @{ + @"application_type" : @"native", + + @"token_endpoint_auth_method" : @"client_secret_basic", + + @"client_name" : self->_clientName, + @"redirect_uris" : @[ + [self classSettingForOCClassSettingsKey:OCAuthenticationMethodOpenIDConnectRedirectURI] + ] + }; + + // Create HTTP request + if ((openidClientRegistrationRequest = [OCHTTPRequest requestWithURL:registrationEndpointURL jsonObject:jsonRequestDict error:&error]) != nil) + { + OCTLogDebug(@[@"ClientRegistration"], @"Registering client %@", self->_clientName); + + // Send client registration request + [connection sendRequest:openidClientRegistrationRequest ephermalCompletionHandler:^(OCHTTPRequest * _Nonnull request, OCHTTPResponse * _Nullable response, NSError * _Nullable error) { + /* + Example response: + + 201 CREATED + Access-Control-Allow-Origin: * + Content-Type: application/json; encoding=utf-8 + Pragma: no-cache + x-xss-protection: 1; mode=block + Expires: Thu, 01 Jan 1970 00:00:00 GMT + x-konnectd-version: 17a22705 + referrer-policy: origin + Cache-Control: no-cache, no-store, must-revalidate + Date: Fri, 08 Jan 2021 11:31:16 GMT + Content-Length: 1671 + x-content-type-options: nosniff + x-frame-options: DENY + Last-Modified: Fri, 08 Jan 2021 11:31:16 GMT + + { + "client_id": "dyn.eyJhbGciOiJQUzI1NiIsImtpZCI6Imtvbm5lY3RkX3ByaXZhdGUiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE2MTAxMDkwNzYsImlhdCI6MTYxMDEwNTQ3Niwic3ViIjoiYW9nTnF1cm5xSHZyYVhFQi1sSUduOC1oc1AwZlJmS293aGljZ2ZrUWxSMnlGUDNmcG9qM1FsSGtYQ0g5c0ZmNEVuR0VVOHRwQTQyb0FycTZhaXdwNXciLCJuYW1lIjoib3duQ2xvdWQvaU9TIDExLjUgb24gb2Npcy5vd25jbG91ZC53b3JrcyIsImdyYW50X3R5cGVzIjpbImF1dGhvcml6YXRpb25fY29kZSJdLCJhcHBsaWNhdGlvbl90eXBlIjoibmF0aXZlIiwicmVkaXJlY3RfdXJpcyI6WyJvYzovL2lvcy5vd25jbG91ZC5jb20iXSwiaWRfdG9rZW5fc2lnbmVkX3Jlc3BvbnNlX2FsZyI6IlJTMjU2IiwidG9rZW5fZW5kcG9pbnRfYXV0aF9tZXRob2QiOiJjbGllbnRfc2VjcmV0X2Jhc2ljIn0.kIg1eYH6lnj7CVeHENtX9ZLzewAi93soi506GHX4zStlChQxKDz_p0BLtSVtY-XzquAT8Xt2w177-yduV1YHMsXN5mbXJ8zd2M4lHN5SRwu--eEZJyH9QmtO5f87-wFg3_pxAnYhYtbKoOQatvYiEw66RvgfJ3TT1LikcRDfdg83yO7FsMONv9qmTJsp-wKX8ZT51TVecn8AMXiF-AKYijK9ZE3XO0XpJaL_U3NDxrta2ASllxvvP0da8eAsxJX7DQK2zME62wvPMcQI1l0t4eqaL0i3wGRXH2w5VNgZIaMzJt4-W6UWNfrcJ_75pT6ommsM1kVecid4qyyP4Eckbw", + "client_secret": "TsbAsXmeOZnNzEJN8T2UQGO2oZE8zCNKcyj4pVM2mpVfOcGznVIWRuMoupx8V7hnZqVVMuOBlxT5A1QO6wgR8Q", + "client_id_issued_at": 1610105476, + "client_secret_expires_at": 1610109076, + "redirect_uris": [ + "oc://ios.owncloud.com" + ], + "response_types": [ + "code" + ], + "grant_types": [ + "authorization_code" + ], + "application_type": "native", + "contacts": null, + "client_name": "ownCloud/iOS 11.5 on ocis.owncloud.works", + "client_uri": "", + "jwks": null, + "id_token_signed_response_alg": "RS256", + "userinfo_signed_response_alg": "", + "request_object_signing_alg": "", + "token_endpoint_auth_method": "client_secret_basic", + "token_endpoint_auth_signing_alg": "", + "post_logout_redirect_uris": null + } + */ + + if (response.status.code == OCHTTPStatusCodeCREATED) + { + // OIDC spec: "A successful response SHOULD use the HTTP 201 Created status code + // and return a JSON document [RFC4627] using the application/json content type" + NSDictionary *registrationResponseDict; + NSError *jsonError = nil; + + if ((registrationResponseDict = [response bodyConvertedDictionaryFromJSONWithError:&jsonError]) != nil) + { + NSString *clientID=nil, *clientSecret=nil; + NSNumber *clientSecretExpiresAt=nil; + + if (((clientID = OCTypedCast(registrationResponseDict[@"client_id"], NSString)) != nil) && + ((clientSecret = OCTypedCast(registrationResponseDict[@"client_secret"], NSString)) != nil) && + ((clientSecretExpiresAt = OCTypedCast(registrationResponseDict[@"client_secret_expires_at"], NSNumber)) != nil)) + { + self->_clientRegistrationResponse = registrationResponseDict; + self->_clientRegistrationEndpointURL = registrationEndpointURL; + + self->_clientID = clientID; + self->_clientSecret = clientSecret; + + // OIDC spec: "REQUIRED if client_secret is issued. Time at which the client_secret will + // expire or 0 if it will not expire. Its value is a JSON number representing the number + // of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time." + self->_clientRegistrationExpirationDate = [NSDate dateWithTimeIntervalSince1970:clientSecretExpiresAt.doubleValue]; + + error = nil; + } + else + { + error = OCErrorWithDescriptionAndUserInfo(OCErrorAuthorizationClientRegistrationFailed, @"client_id or client_secret missing from registration response.", @"jsonResponse", registrationResponseDict); + } + } + else + { + error = OCErrorFromError(OCErrorAuthorizationClientRegistrationFailed, jsonError); + } + } + else if (response.status.code == OCHTTPStatusCodeBAD_REQUEST) + { + // OIDC spec: "When a registration error condition occurs, the Client Registration Endpoint returns a + // HTTP 400 Bad Request status code including a JSON object describing the error in the response body." + NSDictionary *errorResponseDict; + NSError *jsonError = nil; + + if ((errorResponseDict = [response bodyConvertedDictionaryFromJSONWithError:&jsonError]) != nil) + { + error = OCErrorWithDescriptionAndUserInfo(OCErrorAuthorizationClientRegistrationFailed, ([NSString stringWithFormat:@"Error registering client: %@ (%@)", errorResponseDict[@"error_description"], errorResponseDict[@"error"]]), @"errorResponse", errorResponseDict); + } + else + { + error = OCErrorFromError(OCErrorAuthorizationClientRegistrationFailed, jsonError); + } + } + else + { + error = OCErrorFromError(OCErrorAuthorizationClientRegistrationFailed, response.status.error); + } + + if (error != nil) + { + OCErrorAddDateFromResponse(error, response); + OCTLogError(@[@"ClientRegistration"], @"Registration failed with error=%@", error); + } + else + { + OCTLogDebug(@[@"ClientRegistration"], @"Successfully registered client"); + } + + completionHandler(error); + }]; + } + else + { + OCTLogError(@[@"ClientRegistration"], @"Error serializing request JSON %@: %@", jsonRequestDict, error); + + completionHandler(error); + } +} + +- (NSDictionary *)postProcessAuthenticationDataDict:(NSDictionary *)authDataDict +{ + authDataDict = [super postProcessAuthenticationDataDict:authDataDict]; + + if (_clientRegistrationResponse != nil) + { + NSMutableDictionary *newAuthDataDict = [authDataDict mutableCopy]; + + if (_clientRegistrationResponse != nil) + { + NSError *error = nil; + + newAuthDataDict[OIDCKeyPathClientRegistrationResponseSerialized] = [NSJSONSerialization dataWithJSONObject:_clientRegistrationResponse options:0 error:&error]; + + if (error != nil) + { + OCTLogError(@[@"ClientRegistration"], @"Error %@ encoding to JSON: %@", error, _clientRegistrationResponse); + } + } + if (_clientRegistrationEndpointURL != nil) + { + newAuthDataDict[OIDCKeyPathClientRegistrationEndpointURL] = _clientRegistrationEndpointURL.absoluteString; + } + if (_clientRegistrationExpirationDate != nil) + { + newAuthDataDict[OIDCKeyPathClientRegistrationExpirationDate] = _clientRegistrationExpirationDate; + } + if (_clientID != nil) + { + newAuthDataDict[OIDCKeyPathClientID] = _clientID; + } + if (_clientSecret != nil) + { + newAuthDataDict[OIDCKeyPathClientSecret] = _clientSecret; + } + + return (newAuthDataDict); + } + + return (authDataDict); +} + +- (id)loadCachedAuthenticationSecretForConnection:(OCConnection *)connection +{ + NSDictionary *authSecret; + + if ((authSecret = [super loadCachedAuthenticationSecretForConnection:connection]) != nil) + { + if (_clientRegistrationResponse == nil) + { + NSData *responseSerialized; + + if ((responseSerialized = [authSecret valueForKeyPath:OIDCKeyPathClientRegistrationResponseSerialized]) != nil) + { + NSError *error = nil; + + _clientRegistrationResponse = [NSJSONSerialization JSONObjectWithData:responseSerialized options:0 error:&error]; + + if (error != nil) + { + OCTLogError(@[@"ClientRegistration"], @"Error decoding JSON: %@", error); + } + } + } + + if (_clientRegistrationExpirationDate == nil) + { + _clientRegistrationExpirationDate = [authSecret valueForKeyPath:OIDCKeyPathClientRegistrationExpirationDate]; + } + + if ((_clientRegistrationEndpointURL == nil) && ([authSecret valueForKeyPath:OIDCKeyPathClientRegistrationEndpointURL] != nil)) + { + _clientRegistrationEndpointURL = [NSURL URLWithString:[authSecret valueForKeyPath:OIDCKeyPathClientRegistrationEndpointURL]]; + } + + if (_clientID == nil) + { + _clientID = [authSecret valueForKeyPath:OIDCKeyPathClientID]; + } + + if (_clientSecret == nil) + { + _clientSecret = [authSecret valueForKeyPath:OIDCKeyPathClientSecret]; + } + } + + return (authSecret); + +} + +- (NSString *)clientID +{ + if (self.hasClientRegistration) + { + return (_clientID); + } + + return ([super clientID]); +} + +- (NSString *)clientSecret +{ + if (self.hasClientRegistration) + { + return (_clientSecret); + } + + return ([super clientSecret]); +} + +- (NSDictionary *)clientRegistrationResponse +{ + return (_clientRegistrationResponse); +} + +- (NSURL *)clientRegistrationEndpointURL +{ + return(_clientRegistrationEndpointURL); +} + +- (NSDate *)clientRegistrationExpirationDate +{ + return (_clientRegistrationExpirationDate); +} + +- (BOOL)hasClientRegistration +{ + return ((_clientID != nil) && (_clientSecret != nil)); +} + +- (void)_clearClientRegistrationData +{ + _openIDConfig = nil; + + _clientRegistrationResponse = nil; + _clientRegistrationEndpointURL = nil; + _clientRegistrationExpirationDate = nil; + + _clientName = nil; + _clientID = nil; + _clientSecret = nil; +} + #pragma mark - Authentication Method Detection + (NSURL *)_openIDConfigurationURLForConnection:(OCConnection *)connection { @@ -222,6 +586,20 @@ + (void)detectAuthenticationMethodSupportForConnection:(OCConnection *)connectio }]; } +#pragma mark - Requests +- (void)sendTokenRequestToConnection:(OCConnection *)connection withParameters:(NSDictionary *)parameters requestType:(OCAuthenticationOAuth2TokenRequestType)requestType completionHandler:(void(^)(NSError *error, NSDictionary *jsonResponseDict, NSData *authenticationData))completionHandler +{ + [super sendTokenRequestToConnection:connection withParameters:parameters requestType:requestType completionHandler:^(NSError *error, NSDictionary *jsonResponseDict, NSData *authenticationData) { + if (error != nil) + { + // Force client re-registration in case of an error + [self _clearClientRegistrationData]; + } + completionHandler(error, jsonResponseDict, authenticationData); + }]; +} + + #pragma mark - Generate bookmark authentication data - (NSDictionary *)prepareAuthorizationRequestParameters:(NSDictionary *)parameters forConnection:(OCConnection *)connection options:(OCAuthenticationMethodBookmarkAuthenticationDataGenerationOptions)options { @@ -265,3 +643,5 @@ - (void)generateBookmarkAuthenticationDataWithConnection:(OCConnection *)connect OCClassSettingsKey OCAuthenticationMethodOpenIDConnectRedirectURI = @"oidc-redirect-uri"; OCClassSettingsKey OCAuthenticationMethodOpenIDConnectScope = @"oidc-scope"; +OCClassSettingsKey OCAuthenticationMethodOpenIDRegisterClient = @"oidc-register-client"; +OCClassSettingsKey OCAuthenticationMethodOpenIDRegisterClientNameTemplate = @"oidc-register-client-name-template"; diff --git a/ownCloudSDK/Errors/NSError+OCError.h b/ownCloudSDK/Errors/NSError+OCError.h index 020bb220..e1fbfb40 100644 --- a/ownCloudSDK/Errors/NSError+OCError.h +++ b/ownCloudSDK/Errors/NSError+OCError.h @@ -103,7 +103,9 @@ typedef NS_ENUM(NSUInteger, OCError) OCErrorAuthorizationMethodNotAllowed, //!< Authentication method not allowed. Re-authentication needed. OCErrorAuthorizationMethodUnknown, //!< Authentication method unknown. - OCErrorServerConnectionValidationFailed //!< Validation of connection failed. + OCErrorServerConnectionValidationFailed, //!< Validation of connection failed. + + OCErrorAuthorizationClientRegistrationFailed //!< Client registration failed }; @class OCIssue; diff --git a/ownCloudSDK/Errors/NSError+OCError.m b/ownCloudSDK/Errors/NSError+OCError.m index 2dec2247..d657f0da 100644 --- a/ownCloudSDK/Errors/NSError+OCError.m +++ b/ownCloudSDK/Errors/NSError+OCError.m @@ -320,6 +320,10 @@ + (id)provideUserInfoValueForOCError:(NSError *)error userInfoKey:(NSErrorUserIn case OCErrorServerConnectionValidationFailed: unlocalizedString = @"Connection validation failed."; break; + + case OCErrorAuthorizationClientRegistrationFailed: + unlocalizedString = @"Client registration failed."; + break; } } } diff --git a/ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.h b/ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.h index 35e96aea..246672a9 100644 --- a/ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.h +++ b/ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.h @@ -113,6 +113,9 @@ NS_ASSUME_NONNULL_BEGIN @property(strong,nullable,readonly) NSString *urlSessionIdentifier; +#pragma mark - User Agent ++ (nullable NSString *)stringForTemplate:(NSString *)userAgentTemplate variables:(nullable NSDictionary *)variables; + #pragma mark - Lifecycle - (instancetype)initWithIdentifier:(OCHTTPPipelineID)identifier backend:(nullable OCHTTPPipelineBackend *)backend configuration:(NSURLSessionConfiguration *)sessionConfiguration; diff --git a/ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.m b/ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.m index 2ba5ca67..8ebd3d05 100644 --- a/ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.m +++ b/ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.m @@ -63,37 +63,52 @@ - (void)queueBlock:(dispatch_block_t)block; @implementation OCHTTPPipeline #pragma mark - User agent -+ (nullable NSString *)userAgent ++ (nullable NSString *)stringForTemplate:(NSString *)userAgentTemplate variables:(nullable NSDictionary *)variables { - static NSString *userAgent = nil; - static NSString *userAgentTemplate = nil; - - NSString *template = [self classSettingForOCClassSettingsKey:OCHTTPPipelineSettingUserAgent]; + NSString *bundleName = @"App"; + __block NSString *userAgent = nil; - if (((userAgent == nil) && (template != nil)) || (![template isEqual:userAgentTemplate] && (template != userAgentTemplate))) + if (OCProcessManager.isProcessExtension) { - NSString *bundleName = @"App"; - - if (OCProcessManager.isProcessExtension) + if ((bundleName = [NSBundle.mainBundle objectForInfoDictionaryKey:(__bridge id)kCFBundleNameKey]) == nil) { - if ((bundleName = [NSBundle.mainBundle objectForInfoDictionaryKey:(__bridge id)kCFBundleNameKey]) == nil) - { - bundleName = NSBundle.mainBundle.bundlePath.lastPathComponent.stringByDeletingPathExtension; - } + bundleName = NSBundle.mainBundle.bundlePath.lastPathComponent.stringByDeletingPathExtension; } + } - if (bundleName == nil) - { - bundleName = @"App"; - } + if (bundleName == nil) + { + bundleName = @"App"; + } - userAgent = [[[[[[[template stringByReplacingOccurrencesOfString:@"{{app.build}}" withString:OCAppIdentity.sharedAppIdentity.appBuildNumber] + userAgent = [[[[[[[userAgentTemplate stringByReplacingOccurrencesOfString:@"{{app.build}}" withString:OCAppIdentity.sharedAppIdentity.appBuildNumber] stringByReplacingOccurrencesOfString:@"{{app.version}}" withString:OCAppIdentity.sharedAppIdentity.appVersion] stringByReplacingOccurrencesOfString:@"{{app.part}}" withString:bundleName] stringByReplacingOccurrencesOfString:@"{{device.model}}" withString:UIDevice.currentDevice.model] stringByReplacingOccurrencesOfString:@"{{device.model-id}}" withString:UIDevice.currentDevice.ocModelIdentifier] stringByReplacingOccurrencesOfString:@"{{os.name}}" withString:UIDevice.currentDevice.systemName] stringByReplacingOccurrencesOfString:@"{{os.version}}" withString:UIDevice.currentDevice.systemVersion]; + + if (variables != nil) + { + [variables enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull varName, NSString * _Nonnull value, BOOL * _Nonnull stop) { + userAgent = [userAgent stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"{{%@}}", varName] withString:value]; + }]; + } + + return (userAgent); +} + ++ (nullable NSString *)userAgent +{ + static NSString *userAgent = nil; + static NSString *userAgentTemplate = nil; + + NSString *template = [self classSettingForOCClassSettingsKey:OCHTTPPipelineSettingUserAgent]; + + if (((userAgent == nil) && (template != nil)) || (![template isEqual:userAgentTemplate] && (template != userAgentTemplate))) + { + userAgent = [self stringForTemplate:template variables:nil]; userAgentTemplate = template; } diff --git a/ownCloudSDK/HTTP/Request/OCHTTPRequest+JSON.h b/ownCloudSDK/HTTP/Request/OCHTTPRequest+JSON.h new file mode 100644 index 00000000..ac28f009 --- /dev/null +++ b/ownCloudSDK/HTTP/Request/OCHTTPRequest+JSON.h @@ -0,0 +1,32 @@ +// +// OCHTTPRequest+JSON.h +// ownCloudSDK +// +// Created by Felix Schwarz on 08.01.21. +// Copyright © 2021 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2020, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCHTTPRequest.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OCHTTPRequest (JSON) + ++ (nullable instancetype)requestWithURL:(NSURL *)url jsonObject:(id)jsonObject error:(NSError * _Nullable * _Nullable)outError; + +- (nullable NSError *)setBodyWithJSON:(id)jsonObject; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudSDK/HTTP/Request/OCHTTPRequest+JSON.m b/ownCloudSDK/HTTP/Request/OCHTTPRequest+JSON.m new file mode 100644 index 00000000..c3c7ffeb --- /dev/null +++ b/ownCloudSDK/HTTP/Request/OCHTTPRequest+JSON.m @@ -0,0 +1,56 @@ +// +// OCHTTPRequest+JSON.m +// ownCloudSDK +// +// Created by Felix Schwarz on 08.01.21. +// Copyright © 2021 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2020, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCHTTPRequest+JSON.h" + +@implementation OCHTTPRequest (JSON) + ++ (nullable instancetype)requestWithURL:(NSURL *)url jsonObject:(id)jsonObject error:(NSError * _Nullable * _Nullable)outError +{ + OCHTTPRequest *request = [self requestWithURL:url]; + NSError *error =nil; + + request.redirectPolicy = OCHTTPRequestRedirectPolicyHandleLocally; + + request.method = OCHTTPMethodPOST; + error = [request setBodyWithJSON:jsonObject]; + + if (outError != NULL) + { + *outError = error; + } + + if (error != nil) + { + return (nil); + } + + return (request); +} + +- (nullable NSError *)setBodyWithJSON:(id)jsonObject +{ + NSError *error = nil; + + [self setValue:@"application/json" forHeaderField:OCHTTPHeaderFieldNameContentType]; + self.bodyData = [NSJSONSerialization dataWithJSONObject:jsonObject options:0 error:&error]; + + return (error); +} + +@end diff --git a/ownCloudSDK/Resources/en.lproj/Localizable.strings b/ownCloudSDK/Resources/en.lproj/Localizable.strings index 5bdabb21..da7c1a8a 100644 --- a/ownCloudSDK/Resources/en.lproj/Localizable.strings +++ b/ownCloudSDK/Resources/en.lproj/Localizable.strings @@ -309,6 +309,9 @@ // OCErrorServerConnectionValidationFailed "Connection validation failed." = "Connection validation failed."; +// OCErrorAuthorizationClientRegistrationFailed +"Client registration failed." = "Client registration failed."; + /* Diagnostic */ "Files" = "Files"; "Folders" = "Folders"; diff --git a/ownCloudSDK/ownCloudSDK.h b/ownCloudSDK/ownCloudSDK.h index 76a79818..1d6db258 100644 --- a/ownCloudSDK/ownCloudSDK.h +++ b/ownCloudSDK/ownCloudSDK.h @@ -91,6 +91,7 @@ FOUNDATION_EXPORT const unsigned char ownCloudSDKVersionString[]; #import #import +#import #import #import