-
Notifications
You must be signed in to change notification settings - Fork 50
(auth): implement claude auth #4386
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
Draft
ArtemKhvorostianyi
wants to merge
1
commit into
development
Choose a base branch
from
implement-claude-auth-provider
base: development
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
106 changes: 106 additions & 0 deletions
106
src/Ivy.Docs.Shared/Docs/01_Onboarding/03_CLI/04_Authentication/02_Claude.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| --- | ||
| title: Claude (Anthropic) | ||
| searchHints: | ||
| - claude | ||
| - anthropic | ||
| - authentication | ||
| - oauth | ||
| - pkce | ||
| - claude.ai | ||
| --- | ||
| # Claude (Anthropic) authentication provider | ||
|
|
||
| <Ingress> | ||
| Sign in to your Ivy application with Anthropic Claude using OAuth 2.0 and PKCE—the same style of browser login used by Claude Code and Claude on the web. | ||
| </Ingress> | ||
|
|
||
| ## Overview | ||
|
|
||
| The Claude provider uses Anthropic’s OAuth endpoints to authenticate users with a **Claude.ai** account. The flow is an **authorization code** grant with **PKCE** (no static client secret required for public clients). Ivy exchanges the authorization code at the token endpoint and stores access (and optional refresh) tokens in the Ivy auth session. | ||
|
|
||
| > **Note:** You must register an OAuth client in the [Anthropic Console](https://console.anthropic.com/) and add your Ivy app’s callback URL. Exact console steps may change; follow Anthropic’s current documentation for creating an OAuth application and allowed redirect URIs. | ||
|
|
||
| ## Configuration | ||
|
|
||
| ### 1. Register your OAuth client | ||
|
|
||
| In the Anthropic Console, create an OAuth application and set the **redirect URI** to your Ivy auth callback, for example: | ||
|
|
||
| `https://localhost:5010/ivy/auth/callback` | ||
|
|
||
| Use the same scheme, host, port, and path that your app serves (HTTPS in production). | ||
|
|
||
| ### 2. Install the package | ||
|
|
||
| ```terminal | ||
| dotnet add package Ivy.Auth.Claude | ||
| ``` | ||
|
|
||
| ### 3. Enable the provider | ||
|
|
||
| ```csharp | ||
| using Ivy.Auth.Claude; | ||
|
|
||
| var server = new Server(); | ||
|
|
||
| server.UseAuth<ClaudeAuthProvider>(); | ||
|
|
||
| await server.RunAsync(); | ||
| ``` | ||
|
|
||
| ### 4. Secrets or environment variables | ||
|
|
||
| Configure these keys via [.NET user secrets](../../02_Concepts/14_Secrets.md) or environment variables. | ||
|
|
||
| | Key | Required | Description | | ||
| |-----|----------|-------------| | ||
| | **Claude:ClientId** | Yes | OAuth client ID from the Anthropic Console | | ||
| | **Claude:RedirectUri** | Yes | Must match the registered redirect URI (for example `https://localhost:5010/ivy/auth/callback`) | | ||
| | **Claude:ClientSecret** | No | Use if Anthropic issued a confidential client secret | | ||
| | **Claude:AuthorizationUrl** | No | Default: `https://claude.ai/oauth/authorize` | | ||
| | **Claude:TokenUrl** | No | Default: `https://console.anthropic.com/v1/oauth/token` | | ||
| | **Claude:Scope** | No | Default: `user:profile user:inference` | | ||
| | **Claude:UserInfoUrl** | No | Default: `https://api.anthropic.com/api/oauth/claude_cli/client_data` (profile JSON for `GetUserInfoAsync`; Anthropic may change this API) | | ||
| | **Claude:UserAgent** | Optional | Overrides the `User-Agent` header on HTTP calls (defaults to Ivy’s version string) | | ||
|
|
||
| **User secrets (development):** | ||
|
|
||
| ```terminal | ||
| dotnet user-secrets set "Claude:ClientId" "your_client_id" | ||
| dotnet user-secrets set "Claude:RedirectUri" "https://localhost:5010/ivy/auth/callback" | ||
| ``` | ||
|
|
||
| If your client has a secret: | ||
|
|
||
| ```terminal | ||
| dotnet user-secrets set "Claude:ClientSecret" "your_client_secret" | ||
| ``` | ||
|
|
||
| **Environment variables (production):** use double underscores, for example `Claude__ClientId`, `Claude__RedirectUri`. | ||
|
|
||
| As elsewhere in Ivy, values in user secrets take precedence over environment variables when both are set. | ||
|
|
||
| ## Authentication flow | ||
|
|
||
| 1. The user chooses **Claude** on the Ivy login screen. | ||
| 2. Ivy opens the authorize URL with PKCE (`code_challenge` / `code_verifier`). | ||
| 3. The user signs in on Anthropic and approves the app. | ||
| 4. Anthropic redirects to `/ivy/auth/callback` with an authorization `code`. | ||
| 5. Ivy exchanges the code at the token endpoint (JSON body, PKCE verifier). | ||
| 6. Ivy resolves the user profile for `IAuthService` using the configured user-info URL where possible. | ||
|
|
||
| ## Brokered sessions and token handler | ||
|
|
||
| The package includes `ClaudeAuthTokenHandler` registered for `OAuthProviders.Claude`, so brokered account tooling can refresh and validate Claude OAuth tokens when a refresh token is present. | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| - **Redirect URI mismatch:** The value of **Claude:RedirectUri** must exactly match a redirect URI allowed for your OAuth client (including trailing slashes, `http` vs `https`, and port). | ||
| - **PKCE errors:** The code verifier is kept in memory for the login request. If the server restarts between starting OAuth and completing the callback, start sign-in again. | ||
| - **Profile or user info:** If `Claude:UserInfoUrl` returns 404 or a new JSON shape, set **Claude:UserInfoUrl** to the URL Anthropic documents for profile access, or rely on Ivy’s fallback identity until you adjust the URL. | ||
|
|
||
| ## Related documentation | ||
|
|
||
| - [Authentication overview](01_AuthenticationOverview.md) | ||
| - [GitHub authentication](02_GitHub.md) (similar manual OAuth setup) | ||
| - [Sliplane authentication](02_Sliplane.md) (OAuth code flow reference) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| using System.Text.Json; | ||
| using System.Text.Json.Serialization; | ||
| using Ivy.Core; | ||
| using Microsoft.AspNetCore.Http; | ||
| using Microsoft.Extensions.Configuration; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Ivy.Auth.Claude; | ||
|
|
||
| /// <summary>Anthropic Claude.ai OAuth 2.0 with PKCE (authorization code flow)</summary> | ||
| public class ClaudeAuthProvider : ClaudeAuthTokenHandler, IAuthProvider | ||
| { | ||
| private readonly string _redirectUri; | ||
| private readonly string _authorizationUrl; | ||
| private readonly string _scope; | ||
| private string? _codeVerifier; | ||
|
|
||
| /// <summary>Initialize Claude auth provider</summary> | ||
| public ClaudeAuthProvider(IConfiguration configuration, ILogger<ClaudeAuthTokenHandler>? logger = null) | ||
| : base(configuration, logger) | ||
| { | ||
| _redirectUri = configuration.GetValue<string>("Claude:RedirectUri") | ||
| ?? throw new InvalidOperationException( | ||
| "Missing required configuration: 'Claude:RedirectUri'. Must match the callback URL registered for your OAuth client (for example https://localhost:5010/ivy/auth/callback)."); | ||
|
|
||
| _authorizationUrl = configuration.GetValue<string>("Claude:AuthorizationUrl") | ||
| ?? "https://claude.ai/oauth/authorize"; | ||
|
|
||
| _scope = configuration.GetValue<string>("Claude:Scope") | ||
| ?? "user:profile user:inference"; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public Task<LoginResult> LoginAsync( | ||
| IAuthSession authSession, | ||
| string email, | ||
| string password, | ||
| CancellationToken cancellationToken = default) | ||
| { | ||
| throw new NotSupportedException( | ||
| "Claude authentication only supports OAuth. Use GetOAuthUriAsync and HandleOAuthCallbackAsync instead."); | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public Task<Uri> GetOAuthUriAsync( | ||
| IAuthSession authSession, | ||
| AuthOption option, | ||
| WebhookEndpoint callback, | ||
| CancellationToken cancellationToken = default) | ||
| { | ||
| _codeVerifier = GenerateCodeVerifier(); | ||
| var codeChallenge = GenerateCodeChallenge(_codeVerifier); | ||
|
|
||
| var query = string.Join("&", new[] | ||
| { | ||
| $"code=true", | ||
| $"client_id={Uri.EscapeDataString(ClientId)}", | ||
| "response_type=code", | ||
| $"redirect_uri={Uri.EscapeDataString(_redirectUri)}", | ||
| $"scope={Uri.EscapeDataString(_scope)}", | ||
| $"code_challenge={Uri.EscapeDataString(codeChallenge)}", | ||
| "code_challenge_method=S256", | ||
| $"state={Uri.EscapeDataString(callback.Id)}", | ||
| }); | ||
|
|
||
| var uri = new UriBuilder(_authorizationUrl) { Query = query }.Uri; | ||
| return Task.FromResult(uri); | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public async Task<AuthToken?> HandleOAuthCallbackAsync( | ||
| IAuthSession authSession, | ||
| HttpRequest request, | ||
| CancellationToken cancellationToken = default) | ||
| { | ||
| var code = request.Query["code"].ToString(); | ||
| var error = request.Query["error"].ToString(); | ||
| var errorDescription = request.Query["error_description"].ToString(); | ||
|
|
||
| if (error.Length > 0 || errorDescription.Length > 0) | ||
| throw new ClaudeOAuthException(error, errorDescription); | ||
|
|
||
| if (string.IsNullOrEmpty(code)) | ||
| { | ||
| throw new InvalidOperationException( | ||
| "No authorization code in the OAuth callback. The user may have denied access or the redirect URI may be misconfigured."); | ||
| } | ||
|
|
||
| if (string.IsNullOrEmpty(_codeVerifier)) | ||
| { | ||
| throw new InvalidOperationException( | ||
| "PKCE verifier is missing. Start sign-in again from your app (the OAuth flow was not initiated in this process)."); | ||
| } | ||
|
|
||
| try | ||
| { | ||
| var state = request.Query["state"].ToString(); | ||
| var exchange = new AuthorizationCodeTokenRequest | ||
| { | ||
| GrantType = "authorization_code", | ||
| ClientId = ClientId, | ||
| ClientSecret = ClientSecret, | ||
| Code = code, | ||
| RedirectUri = _redirectUri, | ||
| CodeVerifier = _codeVerifier, | ||
| State = string.IsNullOrEmpty(state) ? null : state | ||
| }; | ||
|
|
||
| using var response = await PostJsonTokenRequestAsync(exchange, cancellationToken).ConfigureAwait(false); | ||
| var responseContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); | ||
|
|
||
| if (!response.IsSuccessStatusCode) | ||
| { | ||
| throw new HttpRequestException( | ||
| $"Claude token exchange failed: {(int)response.StatusCode} {response.ReasonPhrase}. {responseContent}", | ||
| null, | ||
| response.StatusCode); | ||
| } | ||
|
|
||
| var tokenResponse = JsonSerializer.Deserialize<ClaudeTokenResponse>(responseContent); | ||
| if (tokenResponse?.Error != null) | ||
| { | ||
| throw new ClaudeOAuthException(tokenResponse.Error, tokenResponse.ErrorDescription); | ||
| } | ||
|
|
||
| if (string.IsNullOrWhiteSpace(tokenResponse?.AccessToken)) | ||
| return null; | ||
|
|
||
| _codeVerifier = null; | ||
|
|
||
| return new AuthToken( | ||
| tokenResponse.AccessToken, | ||
| tokenResponse.RefreshToken | ||
| ); | ||
| } | ||
| catch (HttpRequestException) | ||
| { | ||
| throw; | ||
| } | ||
| catch (ClaudeOAuthException) | ||
| { | ||
| throw; | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| throw new InvalidOperationException($"Claude OAuth token exchange failed: {ex.Message}", ex); | ||
| } | ||
|
Comment on lines
+144
to
+147
|
||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public Task LogoutAsync(IAuthSession authSession, CancellationToken cancellationToken = default) | ||
| => Task.CompletedTask; | ||
|
|
||
| /// <inheritdoc /> | ||
| public AuthOption[] GetAuthOptions() => | ||
| [new AuthOption(AuthFlow.OAuth, "Claude", OAuthProviders.Claude, Icons.ClaudeCode)]; | ||
|
|
||
| /// <inheritdoc /> | ||
| public Task<BrokeredSessionsResult> GetBrokeredSessionsAsync( | ||
| IAuthSession authSession, | ||
| bool skipCache = false, | ||
| CancellationToken cancellationToken = default) | ||
| => Task.FromResult(BrokeredSessionsResult.Success([])); | ||
|
|
||
| private static string GenerateCodeVerifier() | ||
| { | ||
| var bytes = new byte[32]; | ||
| System.Security.Cryptography.RandomNumberGenerator.Fill(bytes); | ||
| return Convert.ToBase64String(bytes) | ||
| .TrimEnd('=') | ||
| .Replace('+', '-') | ||
| .Replace('/', '_'); | ||
| } | ||
|
|
||
| private static string GenerateCodeChallenge(string codeVerifier) | ||
| { | ||
| var bytes = System.Text.Encoding.UTF8.GetBytes(codeVerifier); | ||
| var hash = System.Security.Cryptography.SHA256.HashData(bytes); | ||
| return Convert.ToBase64String(hash) | ||
| .TrimEnd('=') | ||
| .Replace('+', '-') | ||
| .Replace('/', '_'); | ||
| } | ||
|
|
||
| private sealed class AuthorizationCodeTokenRequest | ||
| { | ||
| [JsonPropertyName("grant_type")] | ||
| public string GrantType { get; init; } = ""; | ||
|
|
||
| [JsonPropertyName("client_id")] | ||
| public string ClientId { get; init; } = ""; | ||
|
|
||
| [JsonPropertyName("client_secret")] | ||
| [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||
| public string? ClientSecret { get; init; } | ||
|
|
||
| [JsonPropertyName("code")] | ||
| public string Code { get; init; } = ""; | ||
|
|
||
| [JsonPropertyName("redirect_uri")] | ||
| public string RedirectUri { get; init; } = ""; | ||
|
|
||
| [JsonPropertyName("code_verifier")] | ||
| public string CodeVerifier { get; init; } = ""; | ||
|
|
||
| [JsonPropertyName("state")] | ||
| [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||
| public string? State { get; init; } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sus because of Code part