sdk/identity/AzureIdentity/Source/MSALCredential.swift (140 lines of code) (raw):

// -------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // The MIT License (MIT) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the ""Software""), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // // -------------------------------------------------------------------------- import AzureCore import Foundation import MSAL public typealias MSALResultCompletionHandler = (MSALResult?, Error?) -> Void /// Delegate protocol for view controllers to hook into the MSAL interactive flow. public protocol MSALInteractiveDelegate: AnyObject { // MARK: Required Methods func parentForWebView() -> UIViewController func didCompleteMSALRequest(withResult result: MSALResult) } public extension MSALInteractiveDelegate where Self: UIViewController { func parentForWebView() -> UIViewController { return self } func didCompleteMSALRequest(withResult _: MSALResult) {} } /// An MSAL credential object. public struct MSALCredential: TokenCredential { // MARK: Properties private let tenant: String? private let clientId: String? private let application: MSALPublicClientApplication? private let account: MSALAccount? private let error: Error? private weak var delegate: MSALInteractiveDelegate? { return ApplicationUtil.currentViewController(forParent: nil) as? MSALInteractiveDelegate } // MARK: Initializers /// Create an OAuth credential. /// - Parameters: /// - tenant: Tenant ID (a GUID) for the AAD instance. /// - clientId: The service principal client or application ID (a GUID). /// - authority: An authority URI for the application. /// - redirectUri: An optional redirect URI for the application. /// - account: Initial value of the `MSALAccount` object, if known. public init( tenant: String, clientId: String, authority: URL, redirectUri: String? = nil, account: MSALAccount? = nil ) { var application: MSALPublicClientApplication? var validationError: Error? do { let aadAuthority = try MSALAADAuthority(url: authority) let config = MSALPublicClientApplicationConfig( clientId: clientId, redirectUri: redirectUri, authority: aadAuthority ) application = try MSALPublicClientApplication(configuration: config) } catch { validationError = error } self.tenant = tenant self.clientId = clientId self.application = application self.account = account self.error = validationError } /// Create an OAuth credential. /// - Parameters: /// - tenant: Tenant ID (a GUID) for the AAD instance. /// - clientId: The service principal client or application ID (a GUID). /// - application: An `MSALPublicClientApplication` object. /// - account: Initial value of the `MSALAccount` object, if known. public init( tenant: String, clientId: String, application: MSALPublicClientApplication, account: MSALAccount? = nil ) { self.tenant = tenant self.clientId = clientId self.application = application self.account = account self.error = nil } // MARK: Public Methods public func validate() throws { if let error = error { throw error } } /// Retrieve a token for the provided scope. /// - Parameters: /// - scopes: A list of a scope strings for which to retrieve the token. /// - completionHandler: A completion handler which forwards the access token. public func token(forScopes scopes: [String], completionHandler: @escaping TokenCompletionHandler) { let group = DispatchGroup() var accessToken: AccessToken? var returnError: AzureError? group.enter() if let account = account { acquireTokenSilently(forAccount: account, withScopes: scopes) { result, error in if let error = error { returnError = AzureError.client("MSAL error.", error) } if let result = result { accessToken = AccessToken( token: result.accessToken, expiresOn: result.expiresOn ) } else { accessToken = nil } group.leave() } } else { acquireTokenInteractively(withScopes: scopes) { result, error in if let err = error { returnError = AzureError.client("MSAL failure.", err) } if let result = result { self.delegate?.didCompleteMSALRequest(withResult: result) accessToken = AccessToken( token: result.accessToken, expiresOn: result.expiresOn ) } else { accessToken = nil } group.leave() } } group.notify(queue: DispatchQueue.main) { completionHandler(accessToken, returnError) } } // MARK: Internal Methods internal func acquireTokenInteractively( withScopes scopes: [String], completionHandler: @escaping MSALResultCompletionHandler ) { guard let parent = delegate?.parentForWebView(), let application = application else { return } let webViewParameters = MSALWebviewParameters(authPresentationViewController: parent) let parameters = MSALInteractiveTokenParameters(scopes: scopes, webviewParameters: webViewParameters) application.acquireToken(with: parameters) { result, error in completionHandler(result, error) } } internal func acquireTokenSilently( forAccount account: MSALAccount, withScopes scopes: [String], completionHandler: @escaping MSALResultCompletionHandler ) { guard let application = application else { return } let parameters = MSALSilentTokenParameters(scopes: scopes, account: account) application.acquireTokenSilent(with: parameters) { result, error in if let error = error { let nsError = error as NSError // interactionRequired means we need to ask the user to sign-in. This usually happens // when the user's Refresh Token is expired or if the user has changed their password // among other possible reasons. if nsError.domain == MSALErrorDomain { if nsError.code == MSALError.interactionRequired.rawValue { self.acquireTokenInteractively(withScopes: scopes) { result, error in completionHandler(result, error) } } } } completionHandler(result, error) } } }