Skip to content

Instantly share code, notes, and snippets.

@nixta
Last active December 19, 2023 12:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nixta/33afa1a17d7a9ea85f0d2d601e55a79f to your computer and use it in GitHub Desktop.
Save nixta/33afa1a17d7a9ea85f0d2d601e55a79f to your computer and use it in GitHub Desktop.
App ID (swift codable)
//
// AGSPortal+AppID.swift
//
// Created by Nicholas Furness on 4/13/18.
// Copyright © 2018 Esri. All rights reserved.
//
import Foundation
import ArcGIS
private struct AppIdToken : Decodable {
let token:String
let expiresIn:Int
private enum CodingKeys: String, CodingKey {
case token = "access_token"
case expiresIn = "expires_in"
}
}
extension AGSPortal {
public enum AppIDError : LocalizedError {
case badUrl
case badJsonResponse
public var errorDescription: String? {
switch self {
case .badUrl:
return "URL to token service could not be constructed from the portal URL."
case .badJsonResponse:
return "The response from the token service could not be interpreted!"
}
}
}
public func getAppIDToken(clientId:String, clientSecret:String, duration:Int? = nil, callback:@escaping (AGSCredential?, Error?)->Void) {
load { [unowned portal = self] error in
guard error == nil else {
print("Error loading portal!")
callback(nil, error)
return
}
guard let tokenUrl = URL(string: "sharing/rest/oauth2/token", relativeTo: portal.url) else {
print("Couldn't construct token URL!")
callback(nil, AppIDError.badUrl)
return
}
var appIdParameters:[String:Any] = [
"f": "json",
"client_id": clientId,
"client_secret": clientSecret,
"grant_type": "client_credentials"
]
if let duration = duration, duration > 0 {
appIdParameters["expiration"] = duration
}
let request = AGSRequestOperation(remoteResource: nil, url: tokenUrl,
queryParameters: appIdParameters,
method: .postFormEncodeParameters)
request.registerListener(AGSOperationQueue.shared(), forCompletion: { (result, error) in
guard error == nil else {
print("Error getting token! \(error!.localizedDescription)")
callback(nil, error)
return
}
guard let tokenData = result as? Data else {
print("Expected Data, but didn't get Data")
callback(nil, AppIDError.badJsonResponse)
return
}
var tokenInfo:AppIdToken!
do {
tokenInfo = try JSONDecoder().decode(AppIdToken.self, from: tokenData)
} catch {
print("Couldn't decode token info! \(error)")
callback(nil, error)
return
}
let credential = AGSCredential(token: tokenInfo.token, referer: nil)
credential.tokenUrl = tokenUrl
callback(credential, nil)
})
AGSOperationQueue.shared().addOperation(request)
}
}
}
import UIKit
import ArcGIS
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, AGSAuthenticationManagerDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
AGSAuthenticationManager.shared().delegate = self
return true
}
var arcgisPortal = AGSPortal.arcGISOnline(withLoginRequired: false)
func authenticationManager(_ authenticationManager: AGSAuthenticationManager, didReceive challenge: AGSAuthenticationChallenge) {
// We enter here because a call to some AGSRemoteResource (e.g. an AGSRouteTask)
// failed with an authentication error. This is our chance to generate and provide
// a token that we want the runtime to use. If we return nil, the authentication
// error will be propagated through to the network caller.
//
// If this method is not implemented, the default Runtime username/password
// prompt will be displayed (unless some OAuth configuration has been set up,
// in which case Runtime will enter the OAuth workflow).
// Here we will try to get a valid credential using the ClientID/ClientSecret.
// Note, I have kept this on AGSPortal because the token REST endpoint is associated
// with the portal so it makes logical sense. Otherwise we would need to pass in the
// URL to the portal anyway. Portal is a very lighweight object so this isn't too
// onerous. If needed, we can revisit this.
//
// IMPORTANT: Embedding ClientID and ClientSecret in your app binary is dangerous and
// a VERY BAD IDEA. Instead, you will need to determine some other way to deliver the
// ClientID and ClientSecret to your application securely at runtime, (e.g. with CloudKit
// and CoreData, or your own secure user login store) and then storing it on device
// in a secure manner like the KeyChain. This sample code does NOT follow that advice.
arcgisPortal.getAppIDToken(clientId: "<YOUR CLIENT ID>", clientSecret: "<YOUR CLIENT SECRET>") { (credential, error) in
if let error = error {
print("Error getting credential using AppID! \(error)")
// By calling continue, we will propagate the authentication error
// to the caller. One would need to check the logs to see that the
// attempt to get this credential also failed.
challenge.continue(with: nil)
return
}
guard let credential = credential else {
// We should always have a credential if there was no error, but
// it doesn't hurt to defend. Note that this will terminate the app.
preconditionFailure("Didn't get a credential… That's strange")
}
// Now we have a credential. Runtime will retry the call that failed,
// adding that credential to the credential cache (and the keychain, if you've
// opted into synchronizing the cache with the keychain),
// and will dequeue the requests that were waiting based off their need
// for the credential that Runtime didn't have.
challenge.continue(with: credential)
print("Provided a valid AppID based credential!")
}
}
...
}
// Sample usage
var portal:AGSPortal = AGSPortal.arcGISOnline(withLoginRequired: false)
portal.getAppIDToken(clientId: clientId, clientSecret: clientSecret) { (credential, error) in
guard error == nil else {
print("Could not get credential! \(error!.localizedDescription)")
return
}
print("Got a credential: \(credential!)")
}
@Devepre
Copy link

Devepre commented Nov 14, 2019

Thanks, great example. Some considerations here:

// prompt will be displayed (unless some OAuth configuration has been set up,
// in which case Runtime will enter the OAuth workflow).

First question here is how to actually implement Named User Login (OAuth configuration) and App Login in parallel as this comment basically describes?

If developer have set AGSAuthenticationManager to some object (AppDelegate in this case) and have implemented delegate's methods, in order to proceed with Named User Login (OAuth flow) need to invoke challenge.continueWithDefaultHandling() inside the authenticationManager(_: didReceive:) method body instead of getAppIDToken(clientId: clientSecret:). But how to distinguish which option to follow? Something like:

if needAppLogin {
            // App Login
            continueWithAppLogin(for: challenge)
        } else {
            // Named User Login (OAuth)
            challenge.continueWithDefaultHandling()
        }

Possibly decision need to be made based on challenge.remoteResource object...

When it's achieved, the second question: if first time the App will fulfil auth challenge with OAuth tokens and second time will fulfil auth challenge with App Login token, will it override Portal credentials and User will be presented with Login Window (OAuth) again during next auth challenge which requires Named User Login?

@nixta
Copy link
Author

nixta commented Nov 14, 2019

here is how to actually implement Named User Login (OAuth configuration) and App Login in parallel as this comment basically describes?

Actually, the full comment doesn't describe this, or at least I didn't mean it to :)

// If this method is not implemented, the default Runtime username/password
// prompt will be displayed (unless some OAuth configuration has been set up,
// in which case Runtime will enter the OAuth workflow).

What I intended to describe by that is this:

  • if there's no OAuth configuration set up in the app, and no challenge handler defined, then you'll get the standard Username/Password alert.
  • If however there is some OAuth configuration set up in the app and still no challenge handler defined, then you will see the OAuth prompt (this is not a UIAlertViewController, but typically some web controller).

I could have made that comment a bit clearer in retrospect.

But there is good news. Your interpretation is spot on. The key bit is how to get a value for needAppLogin. As you spotted, the AGSChallengeHandler has a remoteResource property. This is an AGSRemoteResource instance.

You could take a couple of strategies to work out if you want to continue with the default handler (in your scenario, OAuth) or to provide your own credential:

1. Check the class

You could see what concrete type the AGSRemoteResource references. For example, if you want to use App Login for solving routes only, the AGSRemoteResource will be an AGSRouteTask ¹, so you could try something like:

let needAppLogin = (challenge.remoteResource as? AGSRouteTask) != nil

2. Check the URL

The AGSRemoteResource has a URL property. You could conceivably use this to determine if this is a service you want to use App Login for or OAuth for.

Or of course you could combine the two approaches.


¹ Now, you might say "But I don't see AGSRouteTask in the inheritance diagram on the AGSRemoteResource page" and you'd be right, but it's a subclass of AGSLoadableRemoteResourceBase, which does show it. I will see (but can't promise) whether we can coax doxygen into making that relationship visible from the AGSRemoteResource reference doc, but you get the idea.

Note, in the inheriance diagrams, there is a very subtle mark in the bottom-right corner to indicate subclasses exist:

2019-11-14_10-30-33


NOTE: Strictly speaking, it is currently against our terms and conditions to use App Login to access private data services. App Login is intended to allow access to our value add services such as routing, analysis, etc.. But we realize that there are some valid use cases where private data access via App Login is desirable and we're assessing this condition. Please email me directly to discuss this if you have a use case like this.

@nixta
Copy link
Author

nixta commented Nov 14, 2019

@Devepre, for the rest of your question…

if first time the App will fulfil auth challenge with OAuth tokens and second time will fulfil auth challenge with App Login token, will it override Portal credentials and User will be presented with Login Window (OAuth) again during next auth challenge which requires Named User Login?

Portal credentials are associated with a URL (it might not be the full service URL, but enough to determine a token authority). Internally, when Runtime attempts the REST call it will see if it has a credential cached that looks suitable for that URL and will use it. If not, it will try without a credential (it might not know whether the service is protected). If the credential is not accepted, or there was no credential provided but one is needed, then Runtime will enter its authentication flow: a challengeHandler if one exists, or else the OAuth flow if that's configured, etc.

So you only enter the challengeHandler whenever a suitable credential is not already cached.

So, I think your question becomes moot. For a resource, only one suitable credential will be used at a time. Depending on your logic in your challengeHandler, that could be something you provided with AppLogin, or whatever the defaultHandler (in your example, you'd configured OAuth) provided. But there's no attempt by Runtime to get a new credential until that one becomes invalid.

Note that as of 100.6 there are methods on AGSCredentialCache to invalidate credentials.

@Devepre
Copy link

Devepre commented Nov 14, 2019

Thanks a lot for such detailed explanations and insights, it’s definitely worth to know and take to consideration, kudos to you.

@manish-kumar-git
Copy link

I am able to generate AGOL token but unable to set and use for AuthenticationManager in Android and iOS SDK? Also its not clear how authentication manager will authenticate secured feature layer every time i set the a layer. Some sample will help a lot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment