Skip to content

Commit 6993d6b

Browse files
authored
Merge pull request #4094 from wmathurin/oauth-error-code-enum
[W-23059473] Introduce SFOAuthErrorCode enum for token endpoint error responses
2 parents ab84f31 + bfadb2c commit 6993d6b

8 files changed

Lines changed: 287 additions & 44 deletions

File tree

libs/SalesforceSDKCore/SalesforceSDKCore.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@
8484
4F5727E327F27F1A0008CDA4 /* SFSDKPrimingRecordsResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 4F5727DC27F27F1A0008CDA4 /* SFSDKPrimingRecordsResponse.h */; settings = {ATTRIBUTES = (Public, ); }; };
8585
4F5727E427F27F1A0008CDA4 /* SFSDKPrimingRecordsResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F5727E227F27F1A0008CDA4 /* SFSDKPrimingRecordsResponse.m */; };
8686
4F5A49502E98711600C89DDD /* ScopeParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F5A494F2E98711600C89DDD /* ScopeParser.swift */; };
87+
4FOAUTHEC012E98711600C89DDD /* SFOAuthErrorCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FOAUTHEC002E98711600C89DDD /* SFOAuthErrorCode.swift */; };
88+
4FOAUTHECT012E98711600C89DDD /* SFOAuthErrorCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FOAUTHECT002E98711600C89DDD /* SFOAuthErrorCodeTests.swift */; };
8789
4F5A49582E98B0F800C89DDD /* ScopeParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F5A49572E98B0F800C89DDD /* ScopeParserTests.swift */; };
8890
4F755F5820D48F8600CE4E0E /* NSString+SFAdditionsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F755F4120D48D6700CE4E0E /* NSString+SFAdditionsTests.m */; };
8991
4F7EB40D1BFFC88200768720 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8280EC1716E15FFC00768DE8 /* MessageUI.framework */; };
@@ -599,6 +601,8 @@
599601
4F5727DC27F27F1A0008CDA4 /* SFSDKPrimingRecordsResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SFSDKPrimingRecordsResponse.h; sourceTree = "<group>"; };
600602
4F5727E227F27F1A0008CDA4 /* SFSDKPrimingRecordsResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SFSDKPrimingRecordsResponse.m; sourceTree = "<group>"; };
601603
4F5A494F2E98711600C89DDD /* ScopeParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScopeParser.swift; sourceTree = "<group>"; };
604+
4FOAUTHEC002E98711600C89DDD /* SFOAuthErrorCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFOAuthErrorCode.swift; sourceTree = "<group>"; };
605+
4FOAUTHECT002E98711600C89DDD /* SFOAuthErrorCodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SFOAuthErrorCodeTests.swift; path = SalesforceSDKCoreTests/SFOAuthErrorCodeTests.swift; sourceTree = SOURCE_ROOT; };
602606
4F5A49572E98B0F800C89DDD /* ScopeParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScopeParserTests.swift; path = ../SalesforceSDKCoreTests/ScopeParserTests.swift; sourceTree = "<group>"; };
603607
4F755F4120D48D6700CE4E0E /* NSString+SFAdditionsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "NSString+SFAdditionsTests.m"; path = "SalesforceSDKCoreTests/NSString+SFAdditionsTests.m"; sourceTree = SOURCE_ROOT; };
604608
4F7EB3F71BFFC87600768720 /* SDKCommonNSDataTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDKCommonNSDataTests.m; path = SalesforceSDKCoreTests/SDKCommonNSDataTests.m; sourceTree = SOURCE_ROOT; };
@@ -1105,6 +1109,7 @@
11051109
B7355248228E84AF001C7759 /* SFSDKLogoutBlocker.h */,
11061110
B7A901BD228E4DFA0036D749 /* SFSDKLogoutBlocker.m */,
11071111
399A11508BCB47F490DFB724 /* SFOAuthCoordinatorLightningURLTests.swift */,
1112+
4FOAUTHECT002E98711600C89DDD /* SFOAuthErrorCodeTests.swift */,
11081113
4F9E052C2DD6A06F00548985 /* SFSDKOAuthTokenEndpointResponseTests.m */,
11091114
69848CBB2364063E00893E57 /* SFSDKPushNotificationDataProvider.h */,
11101115
69848CBC2364063E00893E57 /* SFSDKPushNotificationDataProvider.m */,
@@ -1198,6 +1203,7 @@
11981203
4F5A494F2E98711600C89DDD /* ScopeParser.swift */,
11991204
23D96B6E2E145AC20004B06A /* DomainDiscoveryCoordinator.swift */,
12001205
4F8A3B002CEC202F00ECDC76 /* JwtAccessToken.swift */,
1206+
4FOAUTHEC002E98711600C89DDD /* SFOAuthErrorCode.swift */,
12011207
4F96FCC61BFD32130022F021 /* SFOAuthCoordinator.h */,
12021208
4F96FCC71BFD32130022F021 /* SFOAuthCoordinator.m */,
12031209
4F96FCC51BFD32130022F021 /* SFOAuthCoordinator+Internal.h */,
@@ -2250,6 +2256,7 @@
22502256
4F3ECD8A2EBBD150005020A6 /* SFOAuthCoordinatorTests.m in Sources */,
22512257
4FA1B2C32F0E000000000001 /* LoginForAdminTests.swift in Sources */,
22522258
1A31073F5F374B9EB1162F2E /* SFOAuthCoordinatorLightningURLTests.swift in Sources */,
2259+
4FOAUTHECT012E98711600C89DDD /* SFOAuthErrorCodeTests.swift in Sources */,
22532260
4F9E05322DD6A08000548985 /* SFSDKOAuthTokenEndpointResponseTests.m in Sources */,
22542261
4F06AF8D1C49A18E00F70798 /* SalesforceSDKManagerTests.m in Sources */,
22552262
237C186C2E44FCAE0008015C /* EncryptStreamTests.swift in Sources */,
@@ -2444,6 +2451,7 @@
24442451
A3C7476129F709EB00D72B7F /* BiometricAuthenticationManagerInternal.swift in Sources */,
24452452
B7FB26DB1F78096300FB25A2 /* SFSDKIDPErrorHandler.m in Sources */,
24462453
4F5A49502E98711600C89DDD /* ScopeParser.swift in Sources */,
2454+
4FOAUTHEC012E98711600C89DDD /* SFOAuthErrorCode.swift in Sources */,
24472455
B773CCF81F8200BD00D2D1B2 /* SFSDKIDPLoginRequestCommand.m in Sources */,
24482456
CE4CE3931C0E526A009F6029 /* SFUserAccountIdentity.m in Sources */,
24492457
23945B712D78E4A60060B195 /* NotificationType.swift in Sources */,

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -478,15 +478,15 @@ - (void)beginJwtTokenExchangeFlow {
478478
id json = nil;
479479
json = [SFJsonUtils objectFromJSONData:data];
480480
if (json == nil) {
481-
NSError *error = [SFSDKOAuth2 errorWithType:kSFOAuthErrorTypeJWTLaunchFailed
481+
NSError *error = [SFSDKOAuth2 errorWithType:@"jwt_launch_failed"
482482
description:@"Error parsing JWT token exchange response."
483483
underlyingError:[SFJsonUtils lastError]];
484484
[self notifyDelegateOfFailure:error authInfo:self.authInfo];
485485
return;
486486
}
487487
if (![json isKindOfClass:[NSDictionary class]]) {
488488
NSString *errorDesc = [NSString stringWithFormat:@"Expected NSDictionary for JWT token response, received %@ instance.", NSStringFromClass([json class])];
489-
NSError *error = [SFSDKOAuth2 errorWithType:kSFOAuthErrorTypeJWTLaunchFailed
489+
NSError *error = [SFSDKOAuth2 errorWithType:@"jwt_launch_failed"
490490
description:errorDesc];
491491
[self notifyDelegateOfFailure:error authInfo:self.authInfo];
492492
return;
@@ -670,7 +670,7 @@ - (void)handleResponse:(SFSDKOAuthTokenEndpointResponse *)response {
670670
[SFSDKCoreLogger d:[self class] format:@"Refresh attempt timed out after %f seconds.", self.timeout];
671671
[self stopAuthentication];
672672
}
673-
BOOL isUnsupportedGrantType = [response.error.tokenEndpointErrorCode isEqualToString:kSFOAuthErrorTypeUnsupportedGrantType];
673+
BOOL isUnsupportedGrantType = (response.error.errorCode == SFOAuthErrorCodeUnsupportedGrantType);
674674
BOOL isLightningURL = [self.credentials.domain containsString:@".lightning."];
675675
if (isUnsupportedGrantType && isLightningURL) {
676676
[SFSDKCoreLogger e:[self class] format:@"Code exchange failed with unsupported_grant_type against Lightning URL: %@. Lightning URLs do not support authorization_code grant type. Use a My Domain login server URL instead.", self.credentials.domain];
@@ -697,12 +697,12 @@ - (NSError *)checkFrontdoorResponseForErrors:(NSURL *)requestUrl {
697697
NSString *errorDescription = [requestUrl sfsdk_valueForParameterName:kSFOAuthErrorDescription];
698698
if (foundValidEcValue) {
699699
[SFSDKCoreLogger d:[self class] format:@"%@ IDP Authcode redirect response encountered an ec=301 or 302 redirect: %@", NSStringFromSelector(_cmd), requestUrl];
700-
error = [SFSDKOAuth2 errorWithType:kSFOAuthErrorTypeMalformedResponse description:@"IDP Authcode redirect response encountered an ec=301 or 302 redirect"];
700+
error = [SFSDKOAuth2 errorWithType:@"malformed_response" description:@"IDP Authcode redirect response encountered an ec=301 or 302 redirect"];
701701
} else if (errorCode) {
702702
error = [SFSDKOAuth2 errorWithType:errorCode description:errorDescription];
703703
} else if (![requestUrl fragment] && ![requestUrl query]){
704704
[SFSDKCoreLogger d:[self class] format:@"%@ Error: IDP Authcode response has no payload: %@", NSStringFromSelector(_cmd), requestUrl];
705-
error = [SFSDKOAuth2 errorWithType:kSFOAuthErrorTypeMalformedResponse description:@"IDP Authcode redirect response has no payload"];
705+
error = [SFSDKOAuth2 errorWithType:@"malformed_response" description:@"IDP Authcode redirect response has no payload"];
706706
}
707707
return error;
708708
}
@@ -743,7 +743,7 @@ - (void)handleUserAgentResponse:(NSURL *)requestUrl {
743743
response = [requestUrl query];
744744
} else {
745745
[SFSDKCoreLogger d:[self class] format:@"%@ Error: response has no payload: %@", NSStringFromSelector(_cmd), requestUrl];
746-
NSError *error = [SFSDKOAuth2 errorWithType:kSFOAuthErrorTypeMalformedResponse description:@"redirect response has no payload"];
746+
NSError *error = [SFSDKOAuth2 errorWithType:@"malformed_response" description:@"redirect response has no payload"];
747747
[self notifyDelegateOfFailure:error authInfo:self.authInfo];
748748
response = nil;
749749
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
SFOAuthErrorCode.swift
3+
SalesforceSDKCore
4+
5+
Copyright (c) 2026-present, salesforce.com, inc. All rights reserved.
6+
7+
Redistribution and use of this software in source and binary forms, with or without modification,
8+
are permitted provided that the following conditions are met:
9+
* Redistributions of source code must retain the above copyright notice, this list of conditions
10+
and the following disclaimer.
11+
* Redistributions in binary form must reproduce the above copyright notice, this list of
12+
conditions and the following disclaimer in the documentation and/or other materials provided
13+
with the distribution.
14+
* Neither the name of salesforce.com, inc. nor the names of its contributors may be used to
15+
endorse or promote products derived from this software without specific prior written
16+
permission of salesforce.com, inc.
17+
18+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
19+
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20+
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
21+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
24+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
25+
WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26+
*/
27+
28+
import Foundation
29+
30+
/// Typed representation of the OAuth token endpoint error values defined by the
31+
/// Salesforce server in OauthErrorCode.java (core/identity-common-api).
32+
///
33+
/// Use ``from(_:)`` to parse the raw `error` string from a token endpoint response.
34+
@objc public enum SFOAuthErrorCode: Int, CaseIterable {
35+
case unknown = 0
36+
case accessDenied
37+
case appBlocked
38+
case appNotFound
39+
case authorizationPending
40+
case badJtiClaim
41+
case appAttestationFailed
42+
case appAttestationFailedRetry
43+
case ecAppPolicyNotFound
44+
case exceededRegistrationLimit
45+
case failCloseAppBlocked
46+
case failedRegistration
47+
case immediateUnsuccessful
48+
case installationError
49+
case invalidAppAccess
50+
case invalidAssertionType
51+
case invalidBasicAuthHeader
52+
case invalidClient
53+
case invalidClientId
54+
case invalidDpopProof
55+
case invalidDistributionState
56+
case invalidExpid
57+
case invalidGrant
58+
case invalidOtp
59+
case invalidRequest
60+
case invalidScope
61+
case invalidSessionLevel
62+
case invalidToken
63+
case loginError
64+
case oauthFlowDisabled
65+
case oauthPolicyNotFound
66+
case otpError
67+
case redirectUriMissing
68+
case redirectUriMismatch
69+
case registrationError
70+
case serverError
71+
case serviceUnavailable
72+
case slowDown
73+
case systemDown
74+
case unknownError
75+
case unsupportedExpid
76+
case unsupportedGrantType
77+
case unsupportedResponseType
78+
case unsupportedTokenType
79+
case useDpopNonce
80+
81+
/// Returns the ``SFOAuthErrorCode`` whose wire value matches `string`,
82+
/// or `.unknown` if `string` is nil, empty, or not recognized.
83+
public static func from(_ string: String?) -> SFOAuthErrorCode {
84+
guard let string = string, !string.isEmpty else { return .unknown }
85+
return SFOAuthErrorCode.allCases.first { $0.wireValue == string } ?? .unknown
86+
}
87+
}
88+
89+
/// Objective-C–accessible bridge for ``SFOAuthErrorCode``.
90+
/// Use `SFOAuthErrorCodeHelper.from(_:)` from Objective-C to parse error wire strings.
91+
@objc public class SFOAuthErrorCodeHelper: NSObject {
92+
/// Returns the integer raw value of the ``SFOAuthErrorCode`` matching `string`,
93+
/// or the raw value of `.unknown` (0) if not recognized.
94+
@objc public static func from(_ string: String?) -> NSInteger {
95+
return SFOAuthErrorCode.from(string).rawValue
96+
}
97+
}
98+
99+
public extension SFOAuthErrorCode {
100+
/// The wire string value sent in the token endpoint error JSON response.
101+
/// Returns `nil` for `.unknown`.
102+
var wireValue: String? {
103+
switch self {
104+
case .unknown: return nil
105+
case .accessDenied: return "access_denied"
106+
case .appBlocked: return "app_blocked"
107+
case .appNotFound: return "app_not_found"
108+
case .authorizationPending: return "authorization_pending"
109+
case .badJtiClaim: return "bad_jti_claim"
110+
case .appAttestationFailed: return "client_blocked"
111+
case .appAttestationFailedRetry: return "client_blocked_retry"
112+
case .ecAppPolicyNotFound: return "ecapp_policy_not_found"
113+
case .exceededRegistrationLimit: return "exceeded_registration_limit"
114+
case .failCloseAppBlocked: return "fail_close_app_blocked"
115+
case .failedRegistration: return "failed_registration"
116+
case .immediateUnsuccessful: return "immediate_unsuccessful"
117+
case .installationError: return "installation_error"
118+
case .invalidAppAccess: return "invalid_app_access"
119+
case .invalidAssertionType: return "invalid_assertion_type"
120+
case .invalidBasicAuthHeader: return "invalid_basic_auth_header"
121+
case .invalidClient: return "invalid_client"
122+
case .invalidClientId: return "invalid_client_id"
123+
case .invalidDpopProof: return "invalid_dpop_proof"
124+
case .invalidDistributionState: return "invalid_distribution_state"
125+
case .invalidExpid: return "invalid_expid"
126+
case .invalidGrant: return "invalid_grant"
127+
case .invalidOtp: return "invalid_otp"
128+
case .invalidRequest: return "invalid_request"
129+
case .invalidScope: return "invalid_scope"
130+
case .invalidSessionLevel: return "invalid_session_level"
131+
case .invalidToken: return "invalid_token"
132+
case .loginError: return "login_error"
133+
case .oauthFlowDisabled: return "oauth_flow_disabled"
134+
case .oauthPolicyNotFound: return "oauth_policy_not_found"
135+
case .otpError: return "otp_error"
136+
case .redirectUriMissing: return "redirect_uri_missing"
137+
case .redirectUriMismatch: return "redirect_uri_mismatch"
138+
case .registrationError: return "registration_error"
139+
case .serverError: return "server_error"
140+
case .serviceUnavailable: return "service_unavailable"
141+
case .slowDown: return "slow_down"
142+
case .systemDown: return "system_down"
143+
case .unknownError: return "unknown_error"
144+
case .unsupportedExpid: return "unsupported_expid"
145+
case .unsupportedGrantType: return "unsupported_grant_type"
146+
case .unsupportedResponseType: return "unsupported_response_type"
147+
case .unsupportedTokenType: return "unsupported_token_type"
148+
case .useDpopNonce: return "use_dpop_nonce"
149+
@unknown default: return nil
150+
}
151+
}
152+
}

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Util/SFSDKOAuth2.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ NS_ASSUME_NONNULL_BEGIN
8989
@property (nonatomic, readonly) NSString *tokenEndpointErrorCode;
9090
@property (nonatomic, readonly) NSString *tokenEndpointErrorDescription;
9191
@property (nonatomic, readonly) NSError *error;
92+
/// Typed enum representation of ``tokenEndpointErrorCode``.
93+
/// The enum is declared in Swift as `SFOAuthErrorCode`; use that type when calling
94+
/// from Swift. From Objective-C, the type is `NSInteger`.
95+
/// Use this property instead of string-comparing ``tokenEndpointErrorCode``.
96+
@property (nonatomic, readonly) NSInteger errorCode;
9297
@end
9398

9499
@interface SFSDKOAuthTokenEndpointRequest : NSObject

0 commit comments

Comments
 (0)