watson-developer-cloud / swift-sdk

:iphone: The Watson Swift SDK enables developers to quickly add Watson Cognitive Computing services to their Swift applications.
https://watson-developer-cloud.github.io/swift-sdk/
Apache License 2.0
877 stars 223 forks source link

Multiple authentication strategies for obtaining a Watson auth token #106

Closed rfdickerson closed 8 years ago

rfdickerson commented 8 years ago

Proposal

The SDK needs to support multiple authentication types for:

The proposal is to use the Strategy design pattern to encapsulate the concerns of authentication to a class instead of the service that uses the authentication. All implementations of AuthenticationStrategy must implement the authenticate method. We would implement for our users the first 3 types, allowing them to create additional strategies if they need it.

public protocol AuthenticationStrategy {

    func authenticate(onauthenticated: (token: String?, error: NSError?)->Void)

    var token: String? { get }

}

Advantages

Example: Basic Authentication

In this example, the BasicAuthenticationStrategy is constructed with a username and password field as well as the URL for obtaining the token.

Instantiating the SpeechToText service would require an authentication strategy:

let basicAuth = BasicAuthenticationStrategy(
            tokenURL: "https://stream.watsonplatform.net/authorization/api/v1/token",
            serviceURL: "https://stream.watsonplatform.net/speech-to-text/api",
            username: username,
            password: password)

let service = SpeechToText(authStrategy: basicAuth)

To use Facebook OAuth, one would change the call to:

let facebookAuth = OAuthAuthenticationStrategy(
            tokenURL: "https://watson-demo.com",
            serviceURL: "https://stream.watsonplatform.net/speech-to-text/api",
            facebookToken: facebookToken)

let service = SpeechToText(authStrategy: facebookAuth)

Notes

glennrfisher commented 8 years ago

Beautiful! This is a great idea! I think the AuthenticationStrategy protocol is a nice way for us to support both hard-coded username/password credentials (used to obtain a token, of course) and any custom authentication methods that clients may want to implement.

I think the big win here is that it makes it easy for clients to configure their own proxy. This is a great solution to the IPA security problem.

I do have a few questions:

  1. Automatically refreshing tokens. As it stands, I assume a service with an expired token would: a) try to use the token, b) receive an "invalid token" response, c) call authenticate to refresh the token, then d) try the call again with the refreshed token. Do you think that would work, or are there any "gotchas" you see that might prevent it from working?

    As an alternative, what are your thoughts on an expiration: NSDate property to help proactively refresh tokens instead of reactively refreshing them?

  2. Watson vs. Alchemy. Any reason to think that this wouldn't work for the Alchemy services? The token is encoded differently in the HTTP message, but that doesn't seem like it should be a problem.
  3. onauthenticate callback. I'm a bit confused about how this would be used in practice. When would it be called?

    I assume the SpeechToText service would request a token during initialization. But unless the token request is a blocking call, the init function may return before a valid token is retrieved. Is there a way to prevent clients from calling SpeechToText functions before a valid token is retrieved? Here's the kind of situation I'm thinking about

    SpeechToText is initialized, prompting a token to be requested Client requests a transcription, which fails because the token is invalid Token is received

  4. Disadvantages. Are there any disadvantages that you see with the AuthenticationStrategy protocol?
rfdickerson commented 8 years ago

Great! We should discuss some of the details today.

  1. Automatically refreshing tokens. Yes, exactly, the Strategy would be responsible to attempt to authenticate, if the Watson service replies saying the token has expired, the Strategy must have a strategy for obtaining a new token, otherwise return a failure. I think the expiration property on the strategy could be useful for preemptively obtaining a token, and using a cached value otherwise. We must always consider that the token may appear valid based on timestamp, but the server might reject them.
  2. Watson vs. Alchemy. Yeah, I think the user could use any AuthenticationStrategy (like OAuth, Basic, or Token) and it would work, since the AlchemyLanguage and AlchemyVision services would just grab the stored token property inside of the strategy after the authenticate method has finished completion.
  3. onauthenticate callback. The authenticate method would be called by every exposed method in each of our services. The callback would be invoked after strategy has 1) finished going to the network to fetch a new token. 2) has a valid cached token to hand over to the implementing service. I was thinking that the authenticate method would not be called on service initialization, since we probably don't want to add asynchronous (and failable) operations inside of our class initializer. Instead, I was thinking that every public method inside of all of our services would wrap the call to the Watson service with first invoking the authenticate method.
  4. Disadvantages. This goes back to questions raised by question 3. I think that tokens getting invalidated will be inevitable, and it's probably the responsibility of the service method (like TextToSpeech.synthesize()) to set into motion fetching a new token. Things get a bit complicated when we add caching to optimize calls made with a stored token, but it's not so bad.

Usage

  1. User initializes a TextToSpeech class by providing a specific AuthenticationStrategy.
  2. User calls TextToSpeech.synthesize() method.
  3. AuthenticationStrategy does not have a token, makes async call to fetch token.
  4. AuthenticationStrategy stores token and sets an expiration date.
  5. Calls the onauthenticate block which contains the call to do the speech synthesis.
  6. Calls the oncompletion callback.

Subsequent calls with an unbeknownst invalid token could do:

  1. User calls TextToSpeech.synthesize() method.
  2. AuthenticationStrategy claims it has a valid token because expiration date is valid.
  3. AuthenticationStrategy calls onauthenticate block with the provided token.
  4. Calls the code to do the speech synthesis.
  5. Watson TTS returns an NSError with a authentication failure.
  6. AuthenticationStrategy fetches a new token.
  7. Calls the code to do the speech synthesis again.
  8. Calls the oncompletion callback.
glennrfisher commented 8 years ago

Ah, now I see. Thanks for the post! That helps a lot.

I wonder if there's a convenient way to abstract some of that. Maybe services construct a request object and pass it (along with the AuthenticationStrategy object) to a "gateway" that handles retrieving/refreshing tokens?

I know we played around with a gateway early on, but it didn't seem necessary at the time.

Either way, we should probably spend some time to hammer out the design decisions to incorporate the AuthenticationStrategy. I'm still a bit confused about the best way to handle network calls and responses while incorporating tokens.

vherrin commented 8 years ago

Great work on this!