Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC v2: TokenProvider and OAuthPipleinePolicy #48234

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

christothes
Copy link
Member

Contributing to the Azure SDK

Please see our CONTRIBUTING.md if you are not familiar with contributing to this repository or have questions.

For specific information about pull request etiquette and best practices, see this section.

}
}

public class ScopedAuthenticationPolicy : OAuthPipelinePolicy
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Policy Only] example stand-alone policy which encapsulates the TokenProvider implementation and type

Comment on lines 28 to 30
// usage for policy only and no public tokenProvider abstraction.
var policy = new ScopedAuthenticationPolicy(provider, "scope");
client = new FooClient(new Uri("http://localhost"), policy);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Policy Only] Sample usage of the policy approach with no public TokenProvider abstraction.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't fully grok this - it's taking a TokenProvider as a param. Is the idea we'd collapse everything more like:

var entraNuggets = ...;
var policy = new EntraScopedAuthPolicy(entraNuggets, "scope");
client = new FooClient(new Uri("http://localhost"), policy);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, this is a typo. I meant to refactor it that way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the policy would be scoped to Entra? We'd need N auth policies for different Entra flows and anyone trying to use a different auth provider would need to write their own policy from scratch to use it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - in theory we could have aggregated implementations that wrap something like DefaultAzureCredential for Entra flows. But different providers would need their own unless some generic lowest common denominator OAuth impl would suffice.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The policy-only approach feels a lot less baked for a TypeSpec customer. Say I generate an Ollama client and it could be hosted locally with no auth, hosted on Azure ML with Entra, hosted on AWS with IAM, hosted on prem with SimpleIdServer, etc. Then with

  • Token/TokenProvider: I'd just need to implement a new TokenProvider for my identity service and use it with a generic OAuthPipelinePolicy that handled all the refreshing logic. This should be doable by most people without a lot of identity experience.
  • Policy-only: I'd need to write everything myself if we didn't provide anything out of the box. Getting the token, applying it to a message via an auth header, possibly handling things like challenges, refreshing the token when it expired, ..., ? You'd probably need to know quite a bit about identity to get this right.

Does that understanding match yours?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The policy-only approach feels a lot less baked for a TypeSpec customer. Say I generate an Ollama client and it could be hosted locally with no auth, hosted on Azure ML with Entra, hosted on AWS with IAM, hosted on prem with SimpleIdServer, etc. Then with

  • Token/TokenProvider: I'd just need to implement a new TokenProvider for my identity service and use it with a generic OAuthPipelinePolicy that handled all the refreshing logic. This should be doable by most people without a lot of identity experience.

It's debatable how generally applicable the OAuthPipelinePolicy would be for actual cross-cloud scenarios. We could get most of the main parts right generically, like adding the header to the message and refresh. Handling challenges and more advanced scenarios might require some knobs or sub-types to refine, like we even do with our BearerTokenAuthenticationPolicy today for pure Entra scenarios.

  • Policy-only: I'd need to write everything myself if we didn't provide anything out of the box. Getting the token, applying it to a message via an auth header, possibly handling things like challenges, refreshing the token when it expired, ..., ? You'd probably need to know quite a bit about identity to get this right.

Yes, although we could probably provide a base type that does most of the generic heavy lifting. We kind of do this with BearerTokenAuthenticationPolicy. However, it's also now tightly coupling token acquisition with message authorization. If we make that extensible, we now are back to having some kind of token provider abstraction.

Does that understanding match yours?

Mostly, yes. But see comments above.

/// <summary>
/// Represents a provider that can provide a token.
/// </summary>
public abstract class TokenProvider<TContext> where TContext : ITokenContext
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example of TokenProvider generic type that implements various interfaces to indicate the capabilities it supports.

@azure-sdk
Copy link
Collaborator

azure-sdk commented Feb 11, 2025

API change check

APIView has identified API level changes in this PR and created following API reviews.

Azure.Core
System.ClientModel

/// Represents a provider that can provide a token.
/// </summary>
/// <typeparam name="TContext"></typeparam>
public abstract class TokenProvider2<TContext> : ITokenProvider where TContext : ITokenContext
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another example of TokenProvider type that inherits from a common interface to deal with typing issues for client libraries accepting the least specific typed TokenProvider.

Comment on lines +28 to +35
public abstract ValueTask<Token> GetTokenAsync(CancellationToken cancellationToken);

/// <summary>
/// Proposed: Gets the token using the context of the policy instance.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public abstract Token GetToken(CancellationToken cancellationToken);
Copy link
Member Author

@christothes christothes Feb 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Policy Only] This is a possible approach if we go with a policy-only solution so that clients can still fetch stand-alone tokens if needed.

client = new FooClient(new Uri("http://localhost"), policy);
}

public class FooClient
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example generated client for the various scenarios

Copy link
Member

@tg-msft tg-msft left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this a lot.

public System.DateTimeOffset ExpiresOn { get { throw null; } protected set { } }
public System.DateTimeOffset? RefreshOn { get { throw null; } protected set { } }
public string Token { get { throw null; } protected set { } }
public string TokenType { get { throw null; } protected set { } }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we're duplicating all of Token inside here instead of just carrying one?

Would it make sense for the credential to optionally include a reference to its provider for refreshing? It's possible I'm thinking too much about the old TokenCredential design, but I'm looking at Credential as the thing I pass around to clients so I'd expect that to contain all the necessary parts kind of like:

public class Credential
{
    public Credential(ITokenProvider provider) { }
    public Credential(Token staticToken) { }
    
    public Token? Token { get; }
    public ITokenProvider? Provider { get; }
}

Copy link
Member Author

@christothes christothes Feb 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh - this Credential type is left over from the first iteration of this design (here). The new type is Token

This is what the RefreshableCredential looked like there. (Link)

I neglected to bring that part forward into this PR, but I can add it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added RefreshableToken and removed the Credential types for clarity.

{
string[] Scopes { get; }
}
public partial interface ITokenContext
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this design. The idea of Azure's TokenRequestContext layering on top of this makes a lot of sense and feels like we might have designed it this way originally.

public partial interface ITokenProvider
{
object CreateContext(System.Collections.Generic.IReadOnlyDictionary<string, object> properties);
System.ClientModel.Token GetAccessToken(object context, System.Threading.CancellationToken cancellationToken);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit - Should this just be called GetToken at this layer if we're dropping the AccessToken class/naming for SCM?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a temporary naming workaround when I was playing with API shapes where I had conflicts in implementing this interface in TokenCredential.

}
public partial class Token
{
public Token(string token, string tokenType, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn = default(System.DateTimeOffset?)) { }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it's worth having this. Like maybe this would be too tempting for users of a GitHub library generated from TypeSpec to try using rather than wiring up to a full provider? We could debate making it more obscure with a factory Create and no public .ctor.

public abstract partial class OAuthPipelinePolicy : System.ClientModel.Primitives.PipelinePolicy
{
protected OAuthPipelinePolicy() { }
public abstract System.ClientModel.Primitives.OAuthPipelinePolicy CreateAuthenticationPolicy(System.Collections.Generic.IReadOnlyDictionary<string, object> context);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be static or how is this meant to be used if not?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was unintentional. There is a static implementation of the factory here

I was playing with the idea that the client ctor takes a factory instead of the policy implementation and the TypeSpec OAuth2 attribute would generate its defined properties as context to be passed to the factory. The factory could then construct the appropriate policy implementation. It's not fully baked in the test class where the sample usage is though. We'd need a factory interface which the client ctor would accept and the factory would need to implement the interface.

/// <summary>
/// Get the token value.
/// </summary>
public string Token { get; protected set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit - consider calling this TokenValue so it matches what's in Token.

sdk/core/System.ClientModel/src/Auth/Token.cs Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants