Handling authentication
See original GitHub issueAuthentication is a problem that has been bugging me for awhile about this repo. I’m note entirely sure how to resolve it. As a reference here are some of our design goals for this:
- Avoid use of a singleton like
IdentityManager
- Make it easy for a developer to configure authentication once and forget about it no matter what library they are using.
- We want to properly federate requests (a la JS API) as much as possible with as minimal overhead as possible.
I feel like these goals are at odds with each other. No matter what I seem to always come up with designs where you pass the authentication in or query the authentication object for information.
Design 1
Below is a rehash of my inital design:
import { request } from 'arcgis-rest-core';
import { UserAuthentication, AppAuthentication } from 'arcgis-rest-auth';
const auth = new AppAuthentication({
clientId: '123'; // required
clientSecret: '123' // required
token: '123' // optional, an access token if you have one
expires: Date.now() // when the provided token expires
portal: 'https://www.arcgis.com/sharing/rest' // which portal generated this token
});
auth.on('error', (error) => {
// unable to authenticate a request so get a new token and retry.
auth.refreshToken(/* ... */).then((token) => {
error.request.retry(token);
});
});
request({
url: '...'
authentication: auth,
params: {
//...
}
}).then((response) => {
// Success!
}).catch((error) => {
// Error! Both HTTP AND server errors will end up here...
});
This approach will work fine, except that most of the interaction this this repo won’t be directly with request
it will be with help methods that we will write like geocode
. So this starts to break down when we do this:
import {request} from 'arcgis-core';
export function geocode (/* ... */) {
// since request is internal it won't be authenticated. :(
return request(/* ... */).then(/* ... */);
}
Design 2
import { request } from 'arcgis-rest-core';
import { UserAuthentication, AppAuthentication } from 'arcgis-rest-auth';
const session = new AppAuthentication({
clientId: '123'; // required
clientSecret: '123' // required
token: '123' // optional, an access token if you have one
expires: Date.now() // when the provided token expires
portal: 'https://www.arcgis.com/sharing/rest' // which portal generated this token
});
session.on('error', (error) => {
// unable to authenticate a request so get a new token and retry.
session.refreshToken(/* ... */).then((token) => {
error.request.retry(token);
});
});
// The UserAuthentication and AppAuthentication methods will expose a `authenticate`
// method that accepts a Promise from `request()` it then returns that promise with
// additional error handling.
session.authenticate(request({
url: '...'
params: {
//...
}
})).then((response) => {
// Success!
}).catch((error) => {
// Error! Both HTTP AND server errors will end up here...
});
// this would also work for anything that returned a Promise from `request()` so...
session.authenticate(geocode({/* ... */})).then((response) => {
// Success!
}).catch((error) => {
// Error! Both HTTP AND server errors will end up here...
});
This method has 1 major drawback though. The inital request will ALWAYS be unauthenticated, since the token was never passed in the inital params
. This sucks but we should be able to recover from the error but it will happen every time whereas the JS API is smart enough to not fail and retry with a token all the time.
Design 3
This design is like the inverse of the one above. We simply expose the authentication
option on all methods use request
under the hood and teach request
how to use the passed authentication
object to handle auth failures.
import { request } from 'arcgis-rest-core';
import { UserAuthentication, AppAuthentication } from 'arcgis-rest-auth';
const session = new AppAuthentication({
clientId: '123'; // required
clientSecret: '123' // required
token: '123' // optional, an access token if you have one
expires: Date.now() // when the provided token expires
portal: 'https://www.arcgis.com/sharing/rest' // which portal generated this token
});
session.on('error', (error) => {
// unable to authenticate a request so get a new token and retry.
session.refreshToken(/* ... */).then((token) => {
error.request.retry(token);
});
});
// we would have to teach request how to use `AppAuthentication` to recover from
// auth failures. Hopefully via an interface so it doesn't bloat the core repo.
request({
url: '...'
authentication: session
params: {
//...
}
})).then((response) => {
// Success!
}).catch((error) => {
// Error! Both HTTP AND server errors will end up here...
});
// we would have to pass the `authentication` object down throguh the
geocode({
authentication: session
})).then((response) => {
// Success!
}).catch((error) => {
// Error! Both HTTP AND server errors will end up here...
});
Design 4 - Singleton Authentication Manager
import { request } from 'arcgis-rest-core';
import { AuthenticationManager } from 'arcgis-rest-auth';
// register a client id and client secret
AuthenticationManager.registerOauthCredentials({
clientId: '123'; // required
clientSecret: '123' // required
token: '123' // optional, an access token if you have one
expires: Date.now() // when the provided token expires
portal: 'https://www.arcgis.com/sharing/rest' // which portal generated this token
});
// register an existing token or token from user auth
AuthenticationManager.registerToken({
clientId: '123'; // required
token: '123' // optional, an access token if you have one
expires: Date.now() // when the provided token expires
portal: 'https://www.arcgis.com/sharing/rest' // which portal generated this token
});
AuthenticationManager.on('authentication-required', (error) => {
// unable to authenticate a request user will need to prompt for auth to a service.
});
// request now talks to the `AuthenticationManager` for all auth decisions.
request({
url: '...'
params: {
//...
}
})).then((response) => {
// Success!
}).catch((error) => {
// Error! Both HTTP AND server errors will end up here...
});
// since geocode will impliment `request` it will also use the `AuthenticationManager` singleton.
geocode({
authentication: session
})).then((response) => {
// Success!
}).catch((error) => {
// Error! Both HTTP AND server errors will end up here...
});
I’m not super liking any of these options here. @jgravois @dbouwman @ajturner @tomwayson.
Issue Analytics
- State:
- Created 6 years ago
- Comments:11 (11 by maintainers)
Top GitHub Comments
@dbouwman I edited your comment to add highlighting.
@dbouwman @mjuniper If everyone else is feeling like a stateless
request
method that relies on getting passed a statefulauthentication
option is best I’ll work on implementing it.I’m going to open a new issue to work on the design of this based on Design 3.
@dbouwman @mjuniper I’ve opened up #10 with a design based on Design 3 but with a few improvements.