-
Setting
prefersEphemeralWebBrowserSession
to true for usage of ASWebAuthenticationSession on iOS 13.0+. This removes the iOS security alert before the login web view is displayed and, of course, makes the browser session ephemeral (not persisting cookies). This may not be necessary if we adopt a custom external user agent. -
The motivation for this change is documented here: https://opentable.atlassian.net/browse/RO-10584
- Fixed build error related to a nullability specifier
___
AppAuth for iOS and macOS, and tvOS is a client SDK for communicating with OAuth 2.0 and OpenID Connect providers. It strives to directly map the requests and responses of those specifications, while following the idiomatic style of the implementation language. In addition to mapping the raw protocol flows, convenience methods are available to assist with common tasks like performing an action with fresh tokens.
It follows the best practices set out in
RFC 8252 - OAuth 2.0 for Native Apps
including using SFAuthenticationSession
and SFSafariViewController
on iOS
for the auth request. UIWebView
and WKWebView
are explicitly not
supported due to the security and usability reasons explained in
Section 8.12 of RFC 8252.
It also supports the PKCE extension to OAuth, which was created to secure authorization codes in public clients when custom URI scheme redirects are used. The library is friendly to other extensions (standard or otherwise), with the ability to handle additional params in all protocol requests and responses.
For tvOS, AppAuth implements OAuth 2.0 Device Authorization Grant to allow for tvOS sign-ins through a secondary device.
AppAuth supports iOS 7 and above.
iOS 9+ uses the in-app browser tab pattern
(via SFSafariViewController
), and falls back to the system browser (mobile
Safari) on earlier versions.
Both Custom URI Schemes (all supported versions of iOS) and Universal Links (iOS 9+) can be used with the library.
In general, AppAuth can work with any authorization server that supports native apps, as documented in RFC 8252, either through custom URI scheme redirects, or universal links. Authorization servers that assume all clients are web-based, or require clients to maintain confidentiality of the client secrets may not work well.
AppAuth supports macOS (OS X) 10.9 and above.
AppAuth for macOS supports both custom schemes; a loopback HTTP redirects via a small embedded server.
In general, AppAuth can work with any authorization server that supports native apps, as documented in RFC 8252; either through custom URI schemes, or loopback HTTP redirects. Authorization servers that assume all clients are web-based, or require clients to maintain confidentiality of the client secrets may not work well.
AppAuth supports tvOS 9.0 and above. Please note that while it is possible to run the standard AppAuth library on tvOS, the documentation below describes implementing OAuth 2.0 Device Authorization Grant (AppAuthTV).
AppAuthTV is designed for servers that support the device authorization flow as documented in RFC 8628.
Want to try out AppAuth? Just run:
pod try AppAuth
Follow the instructions in Examples/README.md to configure with your own OAuth client (you need to update three configuration points with your client info to try the demo).
AppAuth supports four options for dependency management.
With CocoaPods,
add the following line to your Podfile
:
pod 'AppAuth'
Then, run pod install
.
tvOS: Use the TV
subspec:
pod 'AppAuth/TV'
With Swift Package Manager,
add the following dependency
to your Package.swift
:
dependencies: [
.package(url: "https://github.com/openid/AppAuth-iOS.git", .upToNextMajor(from: "1.3.0"))
]
tvOS: Use the AppAuthTV
target.
With Carthage, add the following
line to your Cartfile
:
github "openid/AppAuth-iOS" "master"
Then, run carthage bootstrap
.
tvOS: Use the AppAuthTV
framework.
You can also use AppAuth as a static library. This requires linking the library and your project, and including the headers. Here is a suggested configuration:
- Create an Xcode Workspace.
- Add
AppAuth.xcodeproj
to your Workspace. - Include libAppAuth as a linked library for your target (in the "General -> Linked Framework and Libraries" section of your target).
- Add
AppAuth-iOS/Source
to your search paths of your target ("Build Settings -> "Header Search Paths").
Note: There is no static library for AppAuthTV.
AppAuth supports both manual interaction with the authorization server
where you need to perform your own token exchanges, as well as convenience
methods that perform some of this logic for you. This example uses the
convenience method, which returns either an OIDAuthState
object, or an error.
OIDAuthState
is a class that keeps track of the authorization and token
requests and responses, and provides a convenience method to call an API with
fresh tokens. This is the only object that you need to serialize to retain the
authorization state of the session.
You can configure AppAuth by specifying the endpoints directly:
Objective-C
NSURL *authorizationEndpoint =
[NSURL URLWithString:@"https://accounts.google.com/o/oauth2/v2/auth"];
NSURL *tokenEndpoint =
[NSURL URLWithString:@"https://www.googleapis.com/oauth2/v4/token"];
OIDServiceConfiguration *configuration =
[[OIDServiceConfiguration alloc]
initWithAuthorizationEndpoint:authorizationEndpoint
tokenEndpoint:tokenEndpoint];
// perform the auth request...
Swift
let authorizationEndpoint = URL(string: "https://accounts.google.com/o/oauth2/v2/auth")!
let tokenEndpoint = URL(string: "https://www.googleapis.com/oauth2/v4/token")!
let configuration = OIDServiceConfiguration(authorizationEndpoint: authorizationEndpoint,
tokenEndpoint: tokenEndpoint)
// perform the auth request...
tvOS
Objective-C
NSURL *deviceAuthorizationEndpoint =
[NSURL URLWithString:@"https://oauth2.googleapis.com/device/code"];
NSURL *tokenEndpoint =
[NSURL URLWithString:@"https://www.googleapis.com/oauth2/v4/token"];
OIDTVServiceConfiguration *configuration =
[[OIDTVServiceConfiguration alloc]
initWithDeviceAuthorizationEndpoint:deviceAuthorizationEndpoint
tokenEndpoint:tokenEndpoint];
// perform the auth request...
Or through discovery:
Objective-C
NSURL *issuer = [NSURL URLWithString:@"https://accounts.google.com"];
[OIDAuthorizationService discoverServiceConfigurationForIssuer:issuer
completion:^(OIDServiceConfiguration *_Nullable configuration,
NSError *_Nullable error) {
if (!configuration) {
NSLog(@"Error retrieving discovery document: %@",
[error localizedDescription]);
return;
}
// perform the auth request...
}];
Swift
let issuer = URL(string: "https://accounts.google.com")!
// discovers endpoints
OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { configuration, error in
guard let config = configuration else {
print("Error retrieving discovery document: \(error?.localizedDescription ?? "Unknown error")")
return
}
// perform the auth request...
}
tvOS
Objective-C
NSURL *issuer = [NSURL URLWithString:@"https://accounts.google.com"];
[OIDTVAuthorizationService discoverServiceConfigurationForIssuer:issuer
completion:^(OIDTVServiceConfiguration *_Nullable configuration,
NSError *_Nullable error) {
if (!configuration) {
NSLog(@"Error retrieving discovery document: %@",
[error localizedDescription]);
return;
}
// perform the auth request...
}];
First, you need to have a property in your UIApplicationDelegate
implementation to hold the session, in order to continue the authorization flow
from the redirect. In this example, the implementation of this delegate is
a class named AppDelegate
, if your app's application delegate has a different
name, please update the class name in samples below accordingly.
Objective-C
@interface AppDelegate : UIResponder <UIApplicationDelegate>
// property of the app's AppDelegate
@property(nonatomic, strong, nullable) id<OIDExternalUserAgentSession> currentAuthorizationFlow;
@end
Swift
class AppDelegate: UIResponder, UIApplicationDelegate {
// property of the app's AppDelegate
var currentAuthorizationFlow: OIDExternalUserAgentSession?
}
And your main class, a property to store the auth state:
Objective-C
// property of the containing class
@property(nonatomic, strong, nullable) OIDAuthState *authState;
Swift
// property of the containing class
private var authState: OIDAuthState?
Then, initiate the authorization request. By using the
authStateByPresentingAuthorizationRequest
convenience method, the token
exchange will be performed automatically, and everything will be protected with
PKCE (if the server supports it). AppAuth also lets you perform these
requests manually. See the authNoCodeExchange
method in the included Example
app for a demonstration:
Objective-C
// builds authentication request
OIDAuthorizationRequest *request =
[[OIDAuthorizationRequest alloc] initWithConfiguration:configuration
clientId:kClientID
scopes:@[OIDScopeOpenID,
OIDScopeProfile]
redirectURL:kRedirectURI
responseType:OIDResponseTypeCode
additionalParameters:nil];
// performs authentication request
AppDelegate *appDelegate =
(AppDelegate *)[UIApplication sharedApplication].delegate;
appDelegate.currentAuthorizationFlow =
[OIDAuthState authStateByPresentingAuthorizationRequest:request
presentingViewController:self
callback:^(OIDAuthState *_Nullable authState,
NSError *_Nullable error) {
if (authState) {
NSLog(@"Got authorization tokens. Access token: %@",
authState.lastTokenResponse.accessToken);
[self setAuthState:authState];
} else {
NSLog(@"Authorization error: %@", [error localizedDescription]);
[self setAuthState:nil];
}
}];
Swift
// builds authentication request
let request = OIDAuthorizationRequest(configuration: configuration,
clientId: clientID,
clientSecret: clientSecret,
scopes: [OIDScopeOpenID, OIDScopeProfile],
redirectURL: redirectURI,
responseType: OIDResponseTypeCode,
additionalParameters: nil)
// performs authentication request
print("Initiating authorization request with scope: \(request.scope ?? "nil")")
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.currentAuthorizationFlow =
OIDAuthState.authState(byPresenting: request, presenting: self) { authState, error in
if let authState = authState {
self.setAuthState(authState)
print("Got authorization tokens. Access token: " +
"\(authState.lastTokenResponse?.accessToken ?? "nil")")
} else {
print("Authorization error: \(error?.localizedDescription ?? "Unknown error")")
self.setAuthState(nil)
}
}
Handling the Redirect
The authorization response URL is returned to the app via the iOS openURL app delegate method, so you need to pipe this through to the current authorization session (created in the previous session):
Objective-C
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary<NSString *, id> *)options {
// Sends the URL to the current authorization flow (if any) which will
// process it if it relates to an authorization response.
if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) {
_currentAuthorizationFlow = nil;
return YES;
}
// Your additional URL handling (if any) goes here.
return NO;
}
Swift
func application(_ app: UIApplication,
open url: URL,
options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
// Sends the URL to the current authorization flow (if any) which will
// process it if it relates to an authorization response.
if let authorizationFlow = self.currentAuthorizationFlow,
authorizationFlow.resumeExternalUserAgentFlow(with: url) {
self.currentAuthorizationFlow = nil
return true
}
// Your additional URL handling (if any)
return false
}
On macOS, the most popular way to get the authorization response redirect is to start a local HTTP server on the loopback interface (limited to incoming requests from the user's machine only). When the authorization is complete, the user is redirected to that local server, and the authorization response can be processed by the app. AppAuth takes care of managing the local HTTP server lifecycle for you.
Custom URI schemes are also supported on macOS, but some browsers display an interstitial, which reduces the usability. For an example on using custom URI schemes with macOS, See
Example-Mac
.
To receive the authorization response using a local HTTP server, first you need to have an instance variable in your main class to retain the HTTP redirect handler:
Objective-C
OIDRedirectHTTPHandler *_redirectHTTPHandler;
Then, as the port used by the local HTTP server varies, you need to start it before building the authorization request, in order to get the exact redirect URI to use:
Objective-C
static NSString *const kSuccessURLString =
@"http://openid.github.io/AppAuth-iOS/redirect/";
NSURL *successURL = [NSURL URLWithString:kSuccessURLString];
// Starts a loopback HTTP redirect listener to receive the code. This needs to be started first,
// as the exact redirect URI (including port) must be passed in the authorization request.
_redirectHTTPHandler = [[OIDRedirectHTTPHandler alloc] initWithSuccessURL:successURL];
NSURL *redirectURI = [_redirectHTTPHandler startHTTPListener:nil];
Then, initiate the authorization request. By using the
authStateByPresentingAuthorizationRequest
convenience method, the token
exchange will be performed automatically, and everything will be protected with
PKCE (if the server supports it). By assigning the return value to the
OIDRedirectHTTPHandler
's currentAuthorizationFlow
, the authorization will
continue automatically once the user makes their choice:
// builds authentication request
OIDAuthorizationRequest *request =
[[OIDAuthorizationRequest alloc] initWithConfiguration:configuration
clientId:kClientID
clientSecret:kClientSecret
scopes:@[ OIDScopeOpenID ]
redirectURL:redirectURI
responseType:OIDResponseTypeCode
additionalParameters:nil];
// performs authentication request
__weak __typeof(self) weakSelf = self;
_redirectHTTPHandler.currentAuthorizationFlow =
[OIDAuthState authStateByPresentingAuthorizationRequest:request
callback:^(OIDAuthState *_Nullable authState,
NSError *_Nullable error) {
// Brings this app to the foreground.
[[NSRunningApplication currentApplication]
activateWithOptions:(NSApplicationActivateAllWindows |
NSApplicationActivateIgnoringOtherApps)];
// Processes the authorization response.
if (authState) {
NSLog(@"Got authorization tokens. Access token: %@",
authState.lastTokenResponse.accessToken);
} else {
NSLog(@"Authorization error: %@", error.localizedDescription);
}
[weakSelf setAuthState:authState];
}];
Ensure that your main class is a delegate of OIDAuthStateChangeDelegate
, OIDAuthStateErrorDelegate
, implement the corresponding methods, and include the following property and instance variable:
Objective-C
// property of the containing class
@property(nonatomic, strong, nullable) OIDAuthState *authState;
// instance variable of the containing class
OIDTVAuthorizationCancelBlock _cancelBlock;
Then, build and perform the authorization request.
Objective-C
// builds authentication request
__weak __typeof(self) weakSelf = self;
OIDTVAuthorizationRequest *request =
[[OIDTVAuthorizationRequest alloc] initWithConfiguration:configuration
clientId:kClientID
clientSecret:kClientSecret
scopes:@[ OIDScopeOpenID, OIDScopeProfile ]
additionalParameters:nil];
// performs authentication request
OIDTVAuthorizationInitialization initBlock =
^(OIDTVAuthorizationResponse *_Nullable response, NSError *_Nullable error) {
if (response) {
// process authorization response
NSLog(@"Got authorization response: %@", response);
} else {
// handle initialization error
NSLog(@"Error: %@", error);
}
};
OIDTVAuthorizationCompletion completionBlock =
^(OIDAuthState *_Nullable authState, NSError *_Nullable error) {
weakSelf.signInView.hidden = YES;
if (authState) {
NSLog(@"Token response: %@", authState.lastTokenResponse);
[weakSelf setAuthState:authState];
} else {
NSLog(@"Error: %@", error);
[weakSelf setAuthState:nil];
}
};
_cancelBlock = [OIDTVAuthorizationService authorizeTVRequest:request
initialization:initBlock
completion:completionBlock];
AppAuth gives you the raw token information, if you need it. However, we
recommend that users of the OIDAuthState
convenience wrapper use the provided
performActionWithFreshTokens:
method to perform their API calls to avoid
needing to worry about token freshness:
Objective-C
[_authState performActionWithFreshTokens:^(NSString *_Nonnull accessToken,
NSString *_Nonnull idToken,
NSError *_Nullable error) {
if (error) {
NSLog(@"Error fetching fresh tokens: %@", [error localizedDescription]);
return;
}
// perform your API request using the tokens
}];
Swift
let userinfoEndpoint = URL(string:"https://openidconnect.googleapis.com/v1/userinfo")!
self.authState?.performAction() { (accessToken, idToken, error) in
if error != nil {
print("Error fetching fresh tokens: \(error?.localizedDescription ?? "Unknown error")")
return
}
guard let accessToken = accessToken else {
return
}
// Add Bearer token to request
var urlRequest = URLRequest(url: userinfoEndpoint)
urlRequest.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"]
// Perform request...
}
Each OAuth flow involves presenting an external user-agent to the user, that
allows them to interact with the OAuth authorization server. Typical examples
of a user-agent are the user's browser, or an in-app browser tab incarnation
like ASWebAuthenticationSession
on iOS.
AppAuth ships with several implementations of an external user-agent out of the box, including defaults for iOS and macOS suitable for most cases. The default user-agents typically share persistent cookies with the system default browser, to improve the chance that the user doesn't need to sign-in all over again.
It is possible to change the user-agent that AppAuth uses, and even write your own - all without needing to fork the library.
All implementations of the external user-agent, be they included or created by
you need to conform to the
OIDExternalUserAgent
protocol.
Instances of the OIDExternalUserAgent
are passed into
OIDAuthState.authStateByPresentingAuthorizationRequest:externalUserAgent:callback
and/or
OIDAuthorizationService.presentAuthorizationRequest:externalUserAgent:callback:
rather than using the platform-specific convenience methods (which use the
default user-agents for their respective platforms), like
OIDAuthState.authStateByPresentingAuthorizationRequest:presentingViewController:callback:
.
Popular use-cases for writing your own user-agent implementation include needing to style the user-agent in ways not supported by AppAuth, and implementing a fully custom flow with your own business logic. You can take one of the existing implementations as a starting point to copy, rename, and customize to your needs.
AppAuth for iOS includes a few extra user-agent implementations which you can
try, or use as a reference for your own implementation. One of them,
OIDExternalUserAgentIOSCustomBrowser
enables you to use a different browser for authentication, like Chrome for iOS
or Firefox for iOS.
Here's how to configure AppAuth to use a custom browser using the
OIDExternalUserAgentIOSCustomBrowser
user agent:
First, add the following array to your Info.plist (in XCode, right click -> Open As -> Source Code)
<key>LSApplicationQueriesSchemes</key>
<array>
<string>googlechromes</string>
<string>opera-https</string>
<string>firefox</string>
</array>
This is required so that AppAuth can test for the browser and open the app store if it's not installed (the default behavior of this user-agent). You only need to include the URL scheme of the actual browser you intend to use.
Objective-C
// performs authentication request
AppDelegate *appDelegate =
(AppDelegate *)[UIApplication sharedApplication].delegate;
id<OIDExternalUserAgent> userAgent =
[OIDExternalUserAgentIOSCustomBrowser CustomBrowserChrome];
appDelegate.currentAuthorizationFlow =
[OIDAuthState authStateByPresentingAuthorizationRequest:request
externalUserAgent:userAgent
callback:^(OIDAuthState *_Nullable authState,
NSError *_Nullable error) {
if (authState) {
NSLog(@"Got authorization tokens. Access token: %@",
authState.lastTokenResponse.accessToken);
[self setAuthState:authState];
} else {
NSLog(@"Authorization error: %@", [error localizedDescription]);
[self setAuthState:nil];
}
}];
Swift
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
self.logMessage("Error accessing AppDelegate")
return
}
let userAgent = OIDExternalUserAgentIOSCustomBrowser.customBrowserChrome()
appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, externalUserAgent: userAgent) { authState, error in
if let authState = authState {
self.setAuthState(authState)
self.logMessage("Got authorization tokens. Access token: \(authState.lastTokenResponse?.accessToken ?? "DEFAULT_TOKEN")")
} else {
self.logMessage("Authorization error: \(error?.localizedDescription ?? "DEFAULT_ERROR")")
self.setAuthState(nil)
}
}
That's it! With those two changes (which you can try on the included sample), AppAuth will use Chrome iOS for the authorization request (and open Chrome in the App Store if it's not installed).
OIDExternalUserAgentIOSCustomBrowser
user-agent is not intended for consumer apps. It is designed for
advanced enterprise use-cases where the app developers have greater control over
the operating environment and have special requirements that require a custom
browser like Chrome.
You don't need to stop with the included external user agents either! Since the
OIDExternalUserAgent
protocol is part of AppAuth's public API, you can implement your own versions of
it. In the above example,
userAgent = [OIDExternalUserAgentIOSCustomBrowser CustomBrowserChrome]
would
be replaced with an instantiation of your user-agent implementation.
Browse the API documentation.
Sample apps that explore core AppAuth features are available for iOS, macOS and tvOS; follow the instructions in Examples/README.md to get started.