適用対象: 従業員テナント
外部テナント (詳細はこちら)
これは、Microsoft Entra ID を使用してユーザーをサインインさせる方法を説明するチュートリアル シリーズの 3 番目のチュートリアルです。
開始する前に、このページの上部にある [テナントの種類 の選択] セレクターを使用して、テナントの種類を選択します。 Microsoft Entra ID には、 従業員 と 外部の 2 つのテナント構成が用意されています。 従業員テナントの構成は、従業員、内部アプリ、およびその他の組織リソースを対象としています。 外部テナントは、顧客向けのアプリ用です。
このチュートリアルでは、次の操作を行います。
- ユーザーをサインインします。
- ユーザーをログアウトします。
- アプリの UI の作成
[前提条件]
ユーザーのサインイン
iOS 用 Microsoft Authentication Library (MSAL) を使用してユーザーをサインインさせるには、主に 2 つのオプションがあります: 対話形式でトークンを取得するか、サイレントで取得します。
対話形式でユーザーをサインインさせるには、次のコードを使用します。
func acquireTokenInteractively() { guard let applicationContext = self.applicationContext else { return } guard let webViewParameters = self.webViewParameters else { return } // #1 let parameters = MSALInteractiveTokenParameters(scopes: kScopes, webviewParameters: webViewParameters) parameters.promptType = .selectAccount // #2 applicationContext.acquireToken(with: parameters) { (result, error) in // #3 if let error = error { self.updateLogging(text: "Could not acquire token: \(error)") return } guard let result = result else { self.updateLogging(text: "Could not acquire token: No result returned") return } // #4 self.accessToken = result.accessToken self.updateLogging(text: "Access token is \(self.accessToken)") self.updateCurrentAccount(account: result.account) self.getContentWithToken() } }
promptType
のMSALInteractiveTokenParameters
プロパティによって、認証および同意プロンプトの動作が構成されます。 次の値がサポートされています。-
.promptIfNecessary
(既定値) - 必要な場合にのみ、ユーザーに対してメッセージが表示されます。 SSO のエクスペリエンスは、Web ビュー内に Cookie があるかどうかと、アカウントの種類によって決まります。 複数のユーザーがサインインしている場合は、アカウントの選択エクスペリエンスが表示されます。 "これは既定の動作です"。 -
.selectAccount
- ユーザーが指定されていない場合、認証 Web ビューには、現在サインインしているアカウントの一覧が表示され、ユーザーを選択できます。 -
.login
- Web ビューで認証を行うことをユーザーに求めます。 この値を指定した場合、一度にサインインできるアカウントは 1 つだけです。 -
.consent
- 要求の現在のスコープ セットに同意することをユーザーに求めます。
-
ユーザーをサイレント モードでサインインするには、次のコードを使用します。
func acquireTokenSilently(_ account : MSALAccount!) { guard let applicationContext = self.applicationContext else { return } /** Acquire a token for an existing account silently - forScopes: Permissions you want included in the access token received in the result in the completionBlock. Not all scopes are guaranteed to be included in the access token returned. - account: An account object that we retrieved from the application object before that the authentication flow will be locked down to. - completionBlock: The completion block that will be called when the authentication flow completes, or encounters an error. */ let parameters = MSALSilentTokenParameters(scopes: kScopes, account: account) applicationContext.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) { DispatchQueue.main.async { self.acquireTokenInteractively() } return } } self.updateLogging(text: "Could not acquire token silently: \(error)") return } guard let result = result else { self.updateLogging(text: "Could not acquire token: No result returned") return } self.accessToken = result.accessToken self.updateLogging(text: "Refreshed Access token is \(self.accessToken)") self.updateSignOutButton(enabled: true) self.getContentWithToken() } }
acquireTokenSilently
メソッドでは、既存の MSAL アカウントのアクセス トークンをサイレントに取得しようとします。applicationContext
を使用して、指定されたスコープでトークンをリクエストします。 エラーが発生した場合は、ユーザーの操作が必要かどうかを確認し、必要であれば、対話型トークンの取得を開始します。 成功すると、アクセス トークンを更新し、結果をログに記録し、サインアウト ボタンを有効にし、トークンを使用してコンテンツを取得します。
サインイン コールバックを処理する (iOS のみ)
AppDelegate.swift ファイルを開きます。 サインイン後のコールバックを処理するには、次のように MSALPublicClientApplication.handleMSALResponse
クラスに appDelegate
を追加します。
// Inside AppDelegate...
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
return MSALPublicClientApplication.handleMSALResponse(url, sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String)
}
Xcode 11 を使用している場合は、代わりに MSAL コールバックを SceneDelegate.swift に配置する必要があります。 以前の iOS との互換性を保持するために UISceneDelegate と UIApplicationDelegate の両方をサポートしている場合は、MSAL コールバックを両方のファイルに配置する必要があります。
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let urlContext = URLContexts.first else {
return
}
let url = urlContext.url
let sourceApp = urlContext.options.sourceApplication
MSALPublicClientApplication.handleMSALResponse(url, sourceApplication: sourceApp)
}
ユーザーをサインアウトする
Von Bedeutung
MSAL でサインアウトすると、ユーザーに関する既知の情報がすべてアプリケーションから削除されるほか、デバイス構成で許可されている場合、デバイスのアクティブなセッションも削除されます。 必要に応じて、ブラウザーからユーザーをサインアウトさせることもできます。
サインアウト機能を追加するには、ViewController
クラスの内部に次のコードを追加します。
@objc func signOut(_ sender: AnyObject) {
guard let applicationContext = self.applicationContext else { return }
guard let account = self.currentAccount else { return }
do {
/**
Removes all tokens from the cache for this application for the provided account
- account: The account to remove from the cache
*/
let signoutParameters = MSALSignoutParameters(webviewParameters: self.webViewParameters!)
signoutParameters.signoutFromBrowser = false // set this to true if you also want to signout from browser or webview
applicationContext.signout(with: account, signoutParameters: signoutParameters, completionBlock: {(success, error) in
if let error = error {
self.updateLogging(text: "Couldn't sign out account with error: \(error)")
return
}
self.updateLogging(text: "Sign out completed successfully")
self.accessToken = ""
self.updateCurrentAccount(account: nil)
})
}
}
アプリの UI の作成
ここで、次のコードを ViewController
クラスに追加して、Microsoft Graph API を呼び出すボタン、サインアウトするボタン、および出力を表示するためのテキスト ビューを含む UI を作成します:
iOS ユーザーインターフェース
var loggingText: UITextView!
var signOutButton: UIButton!
var callGraphButton: UIButton!
var usernameLabel: UILabel!
func initUI() {
usernameLabel = UILabel()
usernameLabel.translatesAutoresizingMaskIntoConstraints = false
usernameLabel.text = ""
usernameLabel.textColor = .darkGray
usernameLabel.textAlignment = .right
self.view.addSubview(usernameLabel)
usernameLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 50.0).isActive = true
usernameLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10.0).isActive = true
usernameLabel.widthAnchor.constraint(equalToConstant: 300.0).isActive = true
usernameLabel.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
// Add call Graph button
callGraphButton = UIButton()
callGraphButton.translatesAutoresizingMaskIntoConstraints = false
callGraphButton.setTitle("Call Microsoft Graph API", for: .normal)
callGraphButton.setTitleColor(.blue, for: .normal)
callGraphButton.addTarget(self, action: #selector(callGraphAPI(_:)), for: .touchUpInside)
self.view.addSubview(callGraphButton)
callGraphButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
callGraphButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 120.0).isActive = true
callGraphButton.widthAnchor.constraint(equalToConstant: 300.0).isActive = true
callGraphButton.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
// Add sign out button
signOutButton = UIButton()
signOutButton.translatesAutoresizingMaskIntoConstraints = false
signOutButton.setTitle("Sign Out", for: .normal)
signOutButton.setTitleColor(.blue, for: .normal)
signOutButton.setTitleColor(.gray, for: .disabled)
signOutButton.addTarget(self, action: #selector(signOut(_:)), for: .touchUpInside)
self.view.addSubview(signOutButton)
signOutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
signOutButton.topAnchor.constraint(equalTo: callGraphButton.bottomAnchor, constant: 10.0).isActive = true
signOutButton.widthAnchor.constraint(equalToConstant: 150.0).isActive = true
signOutButton.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
let deviceModeButton = UIButton()
deviceModeButton.translatesAutoresizingMaskIntoConstraints = false
deviceModeButton.setTitle("Get device info", for: .normal);
deviceModeButton.setTitleColor(.blue, for: .normal);
deviceModeButton.addTarget(self, action: #selector(getDeviceMode(_:)), for: .touchUpInside)
self.view.addSubview(deviceModeButton)
deviceModeButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
deviceModeButton.topAnchor.constraint(equalTo: signOutButton.bottomAnchor, constant: 10.0).isActive = true
deviceModeButton.widthAnchor.constraint(equalToConstant: 150.0).isActive = true
deviceModeButton.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
// Add logging textfield
loggingText = UITextView()
loggingText.isUserInteractionEnabled = false
loggingText.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(loggingText)
loggingText.topAnchor.constraint(equalTo: deviceModeButton.bottomAnchor, constant: 10.0).isActive = true
loggingText.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 10.0).isActive = true
loggingText.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -10.0).isActive = true
loggingText.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 10.0).isActive = true
}
func platformViewDidLoadSetup() {
NotificationCenter.default.addObserver(self,
selector: #selector(appCameToForeGround(notification:)),
name: UIApplication.willEnterForegroundNotification,
object: nil)
}
@objc func appCameToForeGround(notification: Notification) {
self.loadCurrentAccount()
}
macOSのUI
var callGraphButton: NSButton!
var loggingText: NSTextView!
var signOutButton: NSButton!
var usernameLabel: NSTextField!
func initUI() {
usernameLabel = NSTextField()
usernameLabel.translatesAutoresizingMaskIntoConstraints = false
usernameLabel.stringValue = ""
usernameLabel.isEditable = false
usernameLabel.isBezeled = false
self.view.addSubview(usernameLabel)
usernameLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 30.0).isActive = true
usernameLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10.0).isActive = true
// Add call Graph button
callGraphButton = NSButton()
callGraphButton.translatesAutoresizingMaskIntoConstraints = false
callGraphButton.title = "Call Microsoft Graph API"
callGraphButton.target = self
callGraphButton.action = #selector(callGraphAPI(_:))
callGraphButton.bezelStyle = .rounded
self.view.addSubview(callGraphButton)
callGraphButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
callGraphButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 50.0).isActive = true
callGraphButton.heightAnchor.constraint(equalToConstant: 34.0).isActive = true
// Add sign out button
signOutButton = NSButton()
signOutButton.translatesAutoresizingMaskIntoConstraints = false
signOutButton.title = "Sign Out"
signOutButton.target = self
signOutButton.action = #selector(signOut(_:))
signOutButton.bezelStyle = .texturedRounded
self.view.addSubview(signOutButton)
signOutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
signOutButton.topAnchor.constraint(equalTo: callGraphButton.bottomAnchor, constant: 10.0).isActive = true
signOutButton.heightAnchor.constraint(equalToConstant: 34.0).isActive = true
signOutButton.isEnabled = false
// Add logging textfield
loggingText = NSTextView()
loggingText.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(loggingText)
loggingText.topAnchor.constraint(equalTo: signOutButton.bottomAnchor, constant: 10.0).isActive = true
loggingText.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 10.0).isActive = true
loggingText.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -10.0).isActive = true
loggingText.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -10.0).isActive = true
loggingText.widthAnchor.constraint(equalToConstant: 500.0).isActive = true
loggingText.heightAnchor.constraint(equalToConstant: 300.0).isActive = true
}
func platformViewDidLoadSetup() {}
次に、ViewController
クラス内でも、viewDidLoad()
メソッドを次のように置き換えます。
override func viewDidLoad() {
super.viewDidLoad()
initUI()
do {
try self.initMSAL()
} catch let error {
self.updateLogging(text: "Unable to create Application Context \(error)")
}
self.loadCurrentAccount()
self.platformViewDidLoadSetup()
}
次のステップ
これは、Microsoft Entra ID を使用してユーザーをサインインさせる方法を説明するチュートリアル シリーズの 3 番目のチュートリアルです。
開始する前に、このページの上部にある [テナントの種類 の選択] セレクターを使用して、テナントの種類を選択します。 Microsoft Entra ID には、 従業員 と 外部の 2 つのテナント構成が用意されています。 従業員テナントの構成は、従業員、内部アプリ、およびその他の組織リソースを対象としています。 外部テナントは、顧客向けのアプリ用です。
このチュートリアルでは、次の操作を行います。
- ユーザーをサインインします。
- ユーザーをログアウトします。
[前提条件]
ユーザーのサインイン
iOS 用 Microsoft Authentication Library (MSAL) を使用してユーザーをサインインさせるには、主に 2 つのオプションがあります: 対話形式でトークンを取得するか、サイレントで取得します。
対話形式でユーザーをサインインさせるには、次のコードを使用します。
acquireTokenInteractively() { guard let applicationContext = self.applicationContext else { return } guard let webViewParameters = self.webViewParameters else { return } updateLogging(text: "Acquiring token interactively...") let parameters = MSALInteractiveTokenParameters(scopes: Configuration.kScopes, webviewParameters: webViewParameters) parameters.promptType = .selectAccount applicationContext.acquireToken(with: parameters) { (result, error) in if let error = error { self.updateLogging(text: "Could not acquire token: \(error)") return } guard let result = result else { self.updateLogging(text: "Could not acquire token: No result returned") return } self.accessToken = result.accessToken self.updateLogging(text: "Access token is \(self.accessToken)") self.updateCurrentAccount(account: result.account) } }
このコードでは、最初に、アプリケーション・コンテキスト・パラメーターと Web ビュー・パラメーターが使用可能かどうかを確認します。 次に、ログを更新して、トークンを対話形式で取得していることを示します。 次に、対話型トークン取得のパラメーターを設定し、スコープと Web ビュー パラメーターを指定します。 また、アカウントを選択するためのプロンプトタイプも設定します。
その後、定義されたパラメータを使用して、アプリケーションコンテキストで
acquireToken
メソッドを呼び出します。 完了ハンドラでは、エラーがないかチェックします。 エラーが発生した場合は、エラーメッセージでログを更新します。 成功した場合は、結果からアクセストークンを取得し、トークンでログを更新し、現在のアカウントを更新します。アプリがアクセス トークンを取得したら、現在のアカウントに関連付けられている要求を取得できます。 これを行うには、次のコード スニペットを使用します。
let claims = result.account.accountClaims let preferredUsername = claims?["preferred_username"] as? String
このコードは、
accountClaims
オブジェクトのresult.account
プロパティにアクセスして、アカウントから請求を読み取ります。 次に、要求ディクショナリから "preferred_username" 要求の値を取得し、それをpreferredUsername
変数に割り当てます。ユーザーをサイレント モードでサインインするには、次のコードを使用します。
func acquireTokenSilently() { self.loadCurrentAccount { (account) in guard let currentAccount = account else { self.updateLogging(text: "No token found, try to acquire a token interactively first") return } self.acquireTokenSilently(currentAccount) } }
このコードは、トークンを取得するプロセスをサイレントに開始します。 最初に現在のアカウントの読み込みを試みます。 現在のアカウントが見つかった場合、そのアカウントを使用してトークンをサイレントに取得します。 現在のアカウントが見つからない場合は、トークンが見つからないことを示すログを更新し、最初に対話形式でトークンを取得することを提案します。
上記のコードでは、
loadCurrentAccount
とacquireTokenSilently
の 2 つの関数を呼び出しています。loadCurrentAccount
関数には、次のコードが必要です。func loadCurrentAccount(completion: AccountCompletion? = nil) { guard let applicationContext = self.applicationContext else { return } let msalParameters = MSALParameters() msalParameters.completionBlockQueue = DispatchQueue.main // Note that this sample showcases an app that signs in a single account at a time applicationContext.getCurrentAccount(with: msalParameters, completionBlock: { (currentAccount, previousAccount, error) in if let error = error { self.updateLogging(text: "Couldn't query current account with error: \(error)") return } if let currentAccount = currentAccount { self.updateCurrentAccount(account: currentAccount) self.acquireTokenSilently(currentAccount) if let completion = completion { completion(self.currentAccount) } return } // If testing with Microsoft's shared device mode, see the account that has been signed out from another app. More details here: // https://docs.microsoft.com/azure/active-directory/develop/msal-ios-shared-devices if let previousAccount = previousAccount { self.updateLogging(text: "The account with username \(String(describing: previousAccount.username)) has been signed out.") } else { self.updateLogging(text: "") } self.accessToken = "" self.updateCurrentAccount(account: nil) if let completion = completion { completion(nil) } }) }
このコードでは、iOS 用の MSAL を使用して現在のアカウントを読み込みます。 エラーをチェックし、それに応じてログを更新します。 現在のアカウントが見つかった場合は、それを更新し、サイレントにトークンの取得を試みます。 以前のアカウントが存在する場合は、サインアウトがログに記録されます。アカウントが見つからない場合は、アクセストークンをクリアします。 最後に、完了ブロックが指定されている場合は実行します。
acquireTokenSilently
関数には、次のコードが含まれている必要があります。func acquireTokenSilently(_ account : MSALAccount) { guard let applicationContext = self.applicationContext else { return } /** Acquire a token for an existing account silently - forScopes: Permissions you want included in the access token received in the result in the completionBlock. Not all scopes are guaranteed to be included in the access token returned. - account: An account object that we retrieved from the application object before that the authentication flow will be locked down to. - completionBlock: The completion block that will be called when the authentication flow completes, or encounters an error. */ updateLogging(text: "Acquiring token silently...") let parameters = MSALSilentTokenParameters(scopes: Configuration.kScopes, account: account) applicationContext.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) { DispatchQueue.main.async { self.acquireTokenInteractively() } return } } self.updateLogging(text: "Could not acquire token silently: \(error)") return } guard let result = result else { self.updateLogging(text: "Could not acquire token: No result returned") return } self.accessToken = result.accessToken self.updateLogging(text: "Refreshed Access token is \(self.accessToken)") self.updateSignOutButton(enabled: true) } }
この関数では、iOS 用の MSAL を使用して、既存のアカウントのトークンをサイレントに取得します。
applicationContext
を確認した後、トークン取得プロセスをログに記録します。MSALSilentTokenParameters
を使用して、必要なパラメータを定義します。 次に、トークンをサイレントに取得しようとします。 エラーがある場合は、ユーザー操作の要件を確認し、必要に応じて対話型プロセスを開始します。 成功すると、accessToken
プロパティが更新され、更新されたトークンがログに記録され、サインアウト ボタンが有効になって終了します。
ユーザーをサインアウトする
MSAL for iOS を使用して iOS (Swift) アプリからユーザーをサインアウトするには、次のコードを使用します。
@IBAction func signOut(_ sender: UIButton) {
guard let applicationContext = self.applicationContext else { return }
guard let account = self.currentAccount else { return }
guard let webViewParameters = self.webViewParameters else { return }
updateLogging(text: "Signing out...")
do {
/**
Removes all tokens from the cache for this application for the provided account
- account: The account to remove from the cache
*/
let signoutParameters = MSALSignoutParameters(webviewParameters: webViewParameters)
// If testing with Microsoft's shared device mode, trigger signout from browser. More details here:
// https://docs.microsoft.com/azure/active-directory/develop/msal-ios-shared-devices
if (self.currentDeviceMode == .shared) {
signoutParameters.signoutFromBrowser = true
} else {
signoutParameters.signoutFromBrowser = false
}
applicationContext.signout(with: account, signoutParameters: signoutParameters, completionBlock: {(success, error) in
if let error = error {
self.updateLogging(text: "Couldn't sign out account with error: \(error)")
return
}
self.updateLogging(text: "Sign out completed successfully")
self.accessToken = ""
self.updateCurrentAccount(account: nil)
})
}
}
このコードは、 applicationContext
、 currentAccount
、および webViewParameters
の存在を確認します。 次に、サインアウト プロセスをログに記録します。 このコードは、指定されたアカウントのキャッシュからすべてのトークンを削除します。 現在のデバイスモードに応じて、ブラウザからサインアウトするかどうかを決定します。 完了すると、それに応じてロギングテキストが更新されます。 サインアウト プロセス中にエラーが発生した場合は、エラー メッセージがログに記録されます。 サインアウトが成功すると、アクセス トークンが空の文字列に更新され、現在のアカウントがクリアされます。