Skip to content

Instantly share code, notes, and snippets.

@AmrElsehemy
Created July 27, 2019 15:25
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AmrElsehemy/59b2b30c98125fe2352995cdce07d7ca to your computer and use it in GitHub Desktop.
Save AmrElsehemy/59b2b30c98125fe2352995cdce07d7ca to your computer and use it in GitHub Desktop.
Configuring an iOS app to work with Sitecore Identity 9.1 +
//
// AppAuthExampleViewController.swift
//
// Copyright (c) 2017 The AppAuth Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import AppAuth
import UIKit
typealias PostRegistrationCallback = (_ configuration: OIDServiceConfiguration?, _ registrationResponse: OIDRegistrationResponse?) -> Void
/**
The OIDC issuer from which the configuration will be discovered.
*/
let kIssuer: String = "https://sitecorecloud-209010-si.azurewebsites.net";
/**
The OAuth client ID.
For client configuration instructions, see the [README](https://github.com/openid/AppAuth-iOS/blob/master/Examples/Example-iOS_Swift-Carthage/README.md).
Set to nil to use dynamic registration with this example.
*/
let kClientID: String? = "AwesomeAppId";
/**
The OAuth redirect URI for the client @c kClientID.
For client configuration instructions, see the [README](https://github.com/openid/AppAuth-iOS/blob/master/Examples/Example-iOS_Swift-Carthage/README.md).
*/
let kRedirectURI: String = "awesome://sitecore.identity.app";
/**
NSCoding key for the authState property.
*/
let kAppAuthExampleAuthStateKey: String = "authState";
class AppAuthExampleViewController: UIViewController {
@IBOutlet private weak var authAutoButton: UIButton!
@IBOutlet private weak var authManual: UIButton!
@IBOutlet private weak var codeExchangeButton: UIButton!
@IBOutlet private weak var userinfoButton: UIButton!
@IBOutlet private weak var logTextView: UITextView!
@IBOutlet private weak var trashButton: UIBarButtonItem!
private var authState: OIDAuthState?
override func viewDidLoad() {
super.viewDidLoad()
self.validateOAuthConfiguration()
self.loadState()
self.updateUI()
}
}
extension AppAuthExampleViewController {
func validateOAuthConfiguration() {
// The example needs to be configured with your own client details.
// See: https://github.com/openid/AppAuth-iOS/blob/master/Examples/Example-iOS_Swift-Carthage/README.md
assert(kIssuer != "https://issuer.example.com",
"Update kIssuer with your own issuer.\n" +
"Instructions: https://github.com/openid/AppAuth-iOS/blob/master/Examples/Example-iOS_Swift-Carthage/README.md");
assert(kClientID != "YOUR_CLIENT_ID",
"Update kClientID with your own client ID.\n" +
"Instructions: https://github.com/openid/AppAuth-iOS/blob/master/Examples/Example-iOS_Swift-Carthage/README.md");
assert(kRedirectURI != "com.example.app:/oauth2redirect/example-provider",
"Update kRedirectURI with your own redirect URI.\n" +
"Instructions: https://github.com/openid/AppAuth-iOS/blob/master/Examples/Example-iOS_Swift-Carthage/README.md");
// verifies that the custom URI scheme has been updated in the Info.plist
guard let urlTypes: [AnyObject] = Bundle.main.object(forInfoDictionaryKey: "CFBundleURLTypes") as? [AnyObject], urlTypes.count > 0 else {
assertionFailure("No custom URI scheme has been configured for the project.")
return
}
guard let items = urlTypes[0] as? [String: AnyObject],
let urlSchemes = items["CFBundleURLSchemes"] as? [AnyObject], urlSchemes.count > 0 else {
assertionFailure("No custom URI scheme has been configured for the project.")
return
}
guard let urlScheme = urlSchemes[0] as? String else {
assertionFailure("No custom URI scheme has been configured for the project.")
return
}
assert(urlScheme != "awesome",
"Configure the URI scheme in Info.plist (URL Types -> Item 0 -> URL Schemes -> Item 0) " +
"with the scheme of your redirect URI. Full instructions: " +
"https://github.com/openid/AppAuth-iOS/blob/master/Examples/Example-iOS_Swift-Carthage/README.md")
}
}
//MARK: IBActions
extension AppAuthExampleViewController {
@IBAction func authWithAutoCodeExchange(_ sender: UIButton) {
guard let issuer = URL(string: kIssuer) else {
self.logMessage("Error creating URL for : \(kIssuer)")
return
}
self.logMessage("Fetching configuration for issuer: \(issuer)")
// discovers endpoints
OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { configuration, error in
guard let config = configuration else {
self.logMessage("Error retrieving discovery document: \(error?.localizedDescription ?? "DEFAULT_ERROR")")
self.setAuthState(nil)
return
}
self.logMessage("Got configuration: \(config)")
if let clientId = kClientID {
self.doAuthWithAutoCodeExchange(configuration: config, clientID: clientId, clientSecret: nil)
} else {
self.doClientRegistration(configuration: config) { configuration, response in
guard let configuration = configuration, let clientID = response?.clientID else {
self.logMessage("Error retrieving configuration OR clientID")
return
}
self.doAuthWithAutoCodeExchange(configuration: configuration,
clientID: clientID,
clientSecret: response?.clientSecret)
}
}
}
}
@IBAction func authNoCodeExchange(_ sender: UIButton) {
guard let issuer = URL(string: kIssuer) else {
self.logMessage("Error creating URL for : \(kIssuer)")
return
}
self.logMessage("Fetching configuration for issuer: \(issuer)")
OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { configuration, error in
if let error = error {
self.logMessage("Error retrieving discovery document: \(error.localizedDescription)")
return
}
guard let configuration = configuration else {
self.logMessage("Error retrieving discovery document. Error & Configuration both are NIL!")
return
}
self.logMessage("Got configuration: \(configuration)")
if let clientId = kClientID {
self.doAuthWithoutCodeExchange(configuration: configuration, clientID: clientId, clientSecret: nil)
} else {
self.doClientRegistration(configuration: configuration) { configuration, response in
guard let configuration = configuration, let response = response else {
return
}
self.doAuthWithoutCodeExchange(configuration: configuration,
clientID: response.clientID,
clientSecret: response.clientSecret)
}
}
}
}
@IBAction func codeExchange(_ sender: UIButton) {
guard let tokenExchangeRequest = self.authState?.lastAuthorizationResponse.tokenExchangeRequest() else {
self.logMessage("Error creating authorization code exchange request")
return
}
self.logMessage("Performing authorization code exchange with request \(tokenExchangeRequest)")
OIDAuthorizationService.perform(tokenExchangeRequest) { response, error in
if let tokenResponse = response {
self.logMessage("Received token response with accessToken: \(tokenResponse.accessToken ?? "DEFAULT_TOKEN")")
} else {
self.logMessage("Token exchange error: \(error?.localizedDescription ?? "DEFAULT_ERROR")")
}
self.authState?.update(with: response, error: error)
}
}
@IBAction func userinfo(_ sender: UIButton) {
guard let userinfoEndpoint = self.authState?.lastAuthorizationResponse.request.configuration.discoveryDocument?.userinfoEndpoint else {
self.logMessage("Userinfo endpoint not declared in discovery document")
return
}
self.logMessage("Performing userinfo request")
let currentAccessToken: String? = self.authState?.lastTokenResponse?.accessToken
self.authState?.performAction() { (accessToken, idTOken, error) in
if error != nil {
self.logMessage("Error fetching fresh tokens: \(error?.localizedDescription ?? "ERROR")")
return
}
guard let accessToken = accessToken else {
self.logMessage("Error getting accessToken")
return
}
if currentAccessToken != accessToken {
self.logMessage("Access token was refreshed automatically (\(currentAccessToken ?? "CURRENT_ACCESS_TOKEN") to \(accessToken))")
} else {
self.logMessage("Access token was fresh and not updated \(accessToken)")
}
var urlRequest = URLRequest(url: userinfoEndpoint)
urlRequest.allHTTPHeaderFields = ["Authorization":"Bearer \(accessToken)"]
let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
DispatchQueue.main.async {
guard error == nil else {
self.logMessage("HTTP request failed \(error?.localizedDescription ?? "ERROR")")
return
}
guard let response = response as? HTTPURLResponse else {
self.logMessage("Non-HTTP response")
return
}
guard let data = data else {
self.logMessage("HTTP response data is empty")
return
}
var json: [AnyHashable: Any]?
do {
json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
} catch {
self.logMessage("JSON Serialization Error")
}
if response.statusCode != 200 {
// server replied with an error
let responseText: String? = String(data: data, encoding: String.Encoding.utf8)
if response.statusCode == 401 {
// "401 Unauthorized" generally indicates there is an issue with the authorization
// grant. Puts OIDAuthState into an error state.
let oauthError = OIDErrorUtilities.resourceServerAuthorizationError(withCode: 0,
errorResponse: json,
underlyingError: error)
self.authState?.update(withAuthorizationError: oauthError)
self.logMessage("Authorization Error (\(oauthError)). Response: \(responseText ?? "RESPONSE_TEXT")")
} else {
self.logMessage("HTTP: \(response.statusCode), Response: \(responseText ?? "RESPONSE_TEXT")")
}
return
}
if let json = json {
self.logMessage("Success: \(json)")
}
}
}
task.resume()
}
}
@IBAction func trashClicked(_ sender: UIBarButtonItem) {
let alert = UIAlertController(title: nil,
message: nil,
preferredStyle: UIAlertControllerStyle.actionSheet)
let clearAuthAction = UIAlertAction(title: "Clear OAuthState", style: .destructive) { (_: UIAlertAction) in
self.setAuthState(nil)
self.updateUI()
}
alert.addAction(clearAuthAction)
let clearLogs = UIAlertAction(title: "Clear Logs", style: .default) { (_: UIAlertAction) in
DispatchQueue.main.async {
self.logTextView.text = ""
}
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alert.addAction(clearLogs)
alert.addAction(cancelAction)
self.present(alert, animated: true, completion: nil)
}
}
//MARK: AppAuth Methods
extension AppAuthExampleViewController {
func doClientRegistration(configuration: OIDServiceConfiguration, callback: @escaping PostRegistrationCallback) {
guard let redirectURI = URL(string: kRedirectURI) else {
self.logMessage("Error creating URL for : \(kRedirectURI)")
return
}
let request: OIDRegistrationRequest = OIDRegistrationRequest(configuration: configuration,
redirectURIs: [redirectURI],
responseTypes: nil,
grantTypes: nil,
subjectType: nil,
tokenEndpointAuthMethod: "client_secret_post",
additionalParameters: nil)
// performs registration request
self.logMessage("Initiating registration request")
OIDAuthorizationService.perform(request) { response, error in
if let regResponse = response {
self.setAuthState(OIDAuthState(registrationResponse: regResponse))
self.logMessage("Got registration response: \(regResponse)")
callback(configuration, regResponse)
} else {
self.logMessage("Registration error: \(error?.localizedDescription ?? "DEFAULT_ERROR")")
self.setAuthState(nil)
}
}
}
func doAuthWithAutoCodeExchange(configuration: OIDServiceConfiguration, clientID: String, clientSecret: String?) {
guard let redirectURI = URL(string: kRedirectURI) else {
self.logMessage("Error creating URL for : \(kRedirectURI)")
return
}
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
self.logMessage("Error accessing AppDelegate")
return
}
// builds authentication request
let request = OIDAuthorizationRequest(configuration: configuration,
clientId: clientID,
clientSecret: clientSecret,
scopes: [OIDScopeOpenID, "sitecore.profile"],
redirectURL: redirectURI,
responseType: OIDResponseTypeCode,
additionalParameters: nil)
// performs authentication request
logMessage("Initiating authorization request with scope: \(request.scope ?? "DEFAULT_SCOPE")")
appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, presenting: self) { 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)
}
}
}
func doAuthWithoutCodeExchange(configuration: OIDServiceConfiguration, clientID: String, clientSecret: String?) {
guard let redirectURI = URL(string: kRedirectURI) else {
self.logMessage("Error creating URL for : \(kRedirectURI)")
return
}
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
self.logMessage("Error accessing AppDelegate")
return
}
// builds authentication request
let request = OIDAuthorizationRequest(configuration: configuration,
clientId: clientID,
clientSecret: clientSecret,
scopes: [OIDScopeOpenID, OIDScopeProfile],
redirectURL: redirectURI,
responseType: OIDResponseTypeCode,
additionalParameters: nil)
// performs authentication request
logMessage("Initiating authorization request with scope: \(request.scope ?? "DEFAULT_SCOPE")")
appDelegate.currentAuthorizationFlow = OIDAuthorizationService.present(request, presenting: self) { (response, error) in
if let response = response {
let authState = OIDAuthState(authorizationResponse: response)
self.setAuthState(authState)
self.logMessage("Authorization response with code: \(response.authorizationCode ?? "DEFAULT_CODE")")
// could just call [self tokenExchange:nil] directly, but will let the user initiate it.
} else {
self.logMessage("Authorization error: \(error?.localizedDescription ?? "DEFAULT_ERROR")")
}
}
}
}
//MARK: OIDAuthState Delegate
extension AppAuthExampleViewController: OIDAuthStateChangeDelegate, OIDAuthStateErrorDelegate {
func didChange(_ state: OIDAuthState) {
self.stateChanged()
}
func authState(_ state: OIDAuthState, didEncounterAuthorizationError error: Error) {
self.logMessage("Received authorization error: \(error)")
}
}
//MARK: Helper Methods
extension AppAuthExampleViewController {
func saveState() {
var data: Data? = nil
if let authState = self.authState {
data = NSKeyedArchiver.archivedData(withRootObject: authState)
}
UserDefaults.standard.set(data, forKey: kAppAuthExampleAuthStateKey)
UserDefaults.standard.synchronize()
}
func loadState() {
guard let data = UserDefaults.standard.object(forKey: kAppAuthExampleAuthStateKey) as? Data else {
return
}
if let authState = NSKeyedUnarchiver.unarchiveObject(with: data) as? OIDAuthState {
self.setAuthState(authState)
}
}
func setAuthState(_ authState: OIDAuthState?) {
if (self.authState == authState) {
return;
}
self.authState = authState;
self.authState?.stateChangeDelegate = self;
self.stateChanged()
}
func updateUI() {
self.codeExchangeButton.isEnabled = self.authState?.lastAuthorizationResponse.authorizationCode != nil && !((self.authState?.lastTokenResponse) != nil)
if let authState = self.authState {
self.authAutoButton.setTitle("1. Re-Auth", for: .normal)
self.authManual.setTitle("1(A) Re-Auth", for: .normal)
self.userinfoButton.isEnabled = authState.isAuthorized ? true : false
} else {
self.authAutoButton.setTitle("1. Auto", for: .normal)
self.authManual.setTitle("1(A) Manual", for: .normal)
self.userinfoButton.isEnabled = false
}
}
func stateChanged() {
self.saveState()
self.updateUI()
}
func logMessage(_ message: String?) {
guard let message = message else {
return
}
print(message);
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "hh:mm:ss";
let dateString = dateFormatter.string(from: Date())
// appends to output log
DispatchQueue.main.async {
let logText = "\(self.logTextView.text ?? "")\n\(dateString): \(message)"
self.logTextView.text = logText
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment