-
Notifications
You must be signed in to change notification settings - Fork 0
OAuth
Implement OAuth2 authorization with Lichess using the PKCE (Proof Key for Code Exchange) flow.
Namespace: LichessSharp.Api.Contracts
Access: client.OAuth
Note: Lichess requires the PKCE flow for all OAuth applications. Client secrets are not used. Tokens are long-lived (approximately one year) and do not support refresh tokens.
using LichessSharp;
using LichessSharp.Api.Contracts;
// Step 1: Create authorization request (generates PKCE values automatically)
using var client = new LichessClient(); // No token needed for auth URL generation
var (authUrl, codeVerifier) = client.OAuth.CreateAuthorizationRequest(
clientId: "my-chess-app",
redirectUri: "http://localhost:5000/callback",
scopes: new[] { "preference:read", "challenge:write", "board:play" },
state: Guid.NewGuid().ToString() // For CSRF protection
);
// Store codeVerifier and state in session/database
// Redirect user to authUrl...
Console.WriteLine($"Redirect user to: {authUrl}");
// Step 2: After user authorizes, handle callback
// User is redirected back with ?code=xxx&state=yyy
string authorizationCode = "code_from_callback"; // Get from query string
string storedCodeVerifier = codeVerifier; // Retrieve from session
var tokenRequest = new OAuthTokenRequest
{
Code = authorizationCode,
CodeVerifier = storedCodeVerifier,
RedirectUri = "http://localhost:5000/callback",
ClientId = "my-chess-app"
};
var token = await client.OAuth.GetTokenAsync(tokenRequest);
Console.WriteLine($"Access Token: {token.AccessToken}");
Console.WriteLine($"Expires In: {token.ExpiresIn} seconds");
// Step 3: Use the token for authenticated requests
using var authenticatedClient = new LichessClient(token.AccessToken);
var profile = await authenticatedClient.Account.GetProfileAsync();
Console.WriteLine($"Logged in as: {profile.Username}");Request only the scopes your application needs:
| Scope | Description |
|---|---|
preference:read |
Read user preferences |
preference:write |
Write user preferences |
email:read |
Read user email address |
challenge:read |
Read incoming challenges |
challenge:write |
Create, accept, decline challenges |
challenge:bulk |
Create bulk challenges |
study:read |
Read private studies |
study:write |
Create and edit studies |
tournament:write |
Create and manage tournaments |
racer:write |
Create and join puzzle races |
puzzle:read |
Read puzzle activity |
team:read |
Read team membership |
team:write |
Join and leave teams |
team:lead |
Manage teams you lead |
follow:read |
Read followed players |
follow:write |
Follow and unfollow players |
msg:write |
Send private messages |
board:play |
Play games with Board API |
bot:play |
Play games with Bot API |
engine:read |
Use external engine analysis |
engine:write |
Register external engines |
web:login |
Full website access (use sparingly) |
Generates PKCE values and creates the authorization URL for OAuth2 login. This is a convenience method that handles PKCE automatically.
(string AuthorizationUrl, string CodeVerifier) CreateAuthorizationRequest(
string clientId,
string redirectUri,
IEnumerable<string>? scopes = null,
string? state = null,
string? username = null)Parameters:
| Name | Type | Description |
|---|---|---|
| clientId | string | Your application's unique identifier (arbitrary string) |
| redirectUri | string | URL to redirect back to after authorization |
| scopes | IEnumerable? | OAuth scopes to request |
| state | string? | Random string for CSRF protection |
| username | string? | Hint for which Lichess account to use |
Returns: Tuple containing:
-
AuthorizationUrl- The URL to redirect the user to -
CodeVerifier- The PKCE code verifier (store this securely!)
Example - Basic:
using var client = new LichessClient();
var (authUrl, codeVerifier) = client.OAuth.CreateAuthorizationRequest(
clientId: "my-app",
redirectUri: "http://localhost:5000/callback"
);
// Store codeVerifier in session
HttpContext.Session.SetString("code_verifier", codeVerifier);
// Redirect user
return Redirect(authUrl);Example - With scopes and state:
var state = Guid.NewGuid().ToString();
HttpContext.Session.SetString("oauth_state", state);
var (authUrl, codeVerifier) = client.OAuth.CreateAuthorizationRequest(
clientId: "my-chess-app",
redirectUri: "https://myapp.com/auth/callback",
scopes: new[] { "board:play", "challenge:write" },
state: state
);
HttpContext.Session.SetString("code_verifier", codeVerifier);
return Redirect(authUrl);Example - Pre-fill username:
// Useful when you know which account the user wants to authorize
var (authUrl, codeVerifier) = client.OAuth.CreateAuthorizationRequest(
clientId: "my-app",
redirectUri: "http://localhost:5000/callback",
username: "DrNykterstein" // Pre-fills the username field
);Exchange an authorization code for an access token.
Task<OAuthToken> GetTokenAsync(
OAuthTokenRequest request,
CancellationToken cancellationToken = default)Parameters:
| Name | Type | Description |
|---|---|---|
| request | OAuthTokenRequest | Token request with code and PKCE verifier |
| cancellationToken | CancellationToken | Cancellation token |
OAuthTokenRequest Properties:
| Property | Type | Description |
|---|---|---|
| Code | string | Authorization code from callback |
| CodeVerifier | string | PKCE code verifier from authorization request |
| RedirectUri | string | Must match the original redirect URI |
| ClientId | string | Must match the original client ID |
Returns: OAuthToken with access token.
Example:
// In your callback endpoint
[HttpGet("/callback")]
public async Task<IActionResult> Callback(string code, string state)
{
// Verify state matches
var expectedState = HttpContext.Session.GetString("oauth_state");
if (state != expectedState)
return BadRequest("Invalid state");
// Get stored code verifier
var codeVerifier = HttpContext.Session.GetString("code_verifier");
// Exchange code for token
using var client = new LichessClient();
var token = await client.OAuth.GetTokenAsync(new OAuthTokenRequest
{
Code = code,
CodeVerifier = codeVerifier!,
RedirectUri = "https://myapp.com/auth/callback",
ClientId = "my-chess-app"
});
// Store token securely (e.g., in database)
await SaveUserToken(token.AccessToken, token.ExpiresIn);
return RedirectToAction("Dashboard");
}Revoke the current access token. The token used for Bearer authentication will be invalidated.
Task RevokeTokenAsync(CancellationToken cancellationToken = default)Example:
// When user logs out
using var client = new LichessClient(userAccessToken);
await client.OAuth.RevokeTokenAsync();
// Token is now invalid - clear from storage
await DeleteUserToken(userId);Test multiple OAuth tokens to check their validity and scopes.
Task<IReadOnlyDictionary<string, OAuthTokenInfo?>> TestTokensAsync(
IEnumerable<string> tokens,
CancellationToken cancellationToken = default)Parameters:
| Name | Type | Description |
|---|---|---|
| tokens | IEnumerable | Tokens to test (up to 1000) |
| cancellationToken | CancellationToken | Cancellation token |
Returns: Dictionary mapping each token to its info, or null if invalid.
Example:
using var client = new LichessClient();
var tokens = new[] { "lip_abc123", "lip_xyz789", "invalid_token" };
var results = await client.OAuth.TestTokensAsync(tokens);
foreach (var (token, info) in results)
{
if (info == null)
{
Console.WriteLine($"Token {token[..10]}... is INVALID");
}
else
{
Console.WriteLine($"Token {token[..10]}... is valid");
Console.WriteLine($" User: {info.UserId}");
Console.WriteLine($" Scopes: {info.Scopes}");
if (info.Expires.HasValue)
{
var expiry = DateTimeOffset.FromUnixTimeMilliseconds(info.Expires.Value);
Console.WriteLine($" Expires: {expiry}");
}
}
}Example - Cleanup expired tokens:
var storedTokens = await GetAllStoredTokens();
var results = await client.OAuth.TestTokensAsync(storedTokens.Select(t => t.Token));
foreach (var (token, info) in results)
{
if (info == null)
{
await DeleteStoredToken(token);
Console.WriteLine($"Removed invalid token");
}
}Access token response from GetTokenAsync().
| Property | Type | Description |
|---|---|---|
| TokenType | string | Always "Bearer" |
| AccessToken | string | The access token for API requests |
| ExpiresIn | int | Seconds until expiration (typically ~1 year) |
Token information from TestTokensAsync().
| Property | Type | Description |
|---|---|---|
| UserId | string? | Lichess user ID |
| Scopes | string? | Comma-separated scopes (empty string if none) |
| Expires | long? | Unix timestamp (ms) when token expires, null if never |
Request parameters for GetTokenAsync().
| Property | Type | Description |
|---|---|---|
| Code | string | Authorization code from callback |
| CodeVerifier | string | PKCE code verifier |
| RedirectUri | string | Original redirect URI |
| ClientId | string | Original client ID |
Here's a complete example for integrating Lichess OAuth in an ASP.NET Core application:
// Services/LichessAuthService.cs
public class LichessAuthService
{
private readonly ILichessClient _client;
private readonly IConfiguration _config;
public LichessAuthService(ILichessClient client, IConfiguration config)
{
_client = client;
_config = config;
}
public (string AuthUrl, string CodeVerifier, string State) CreateLoginRequest()
{
var state = Guid.NewGuid().ToString();
var (authUrl, codeVerifier) = _client.OAuth.CreateAuthorizationRequest(
clientId: _config["Lichess:ClientId"]!,
redirectUri: _config["Lichess:RedirectUri"]!,
scopes: new[] { "preference:read" },
state: state
);
return (authUrl, codeVerifier, state);
}
public async Task<OAuthToken> ExchangeCodeAsync(string code, string codeVerifier)
{
return await _client.OAuth.GetTokenAsync(new OAuthTokenRequest
{
Code = code,
CodeVerifier = codeVerifier,
RedirectUri = _config["Lichess:RedirectUri"]!,
ClientId = _config["Lichess:ClientId"]!
});
}
}
// Controllers/AuthController.cs
[Route("auth")]
public class AuthController : Controller
{
private readonly LichessAuthService _authService;
public AuthController(LichessAuthService authService)
{
_authService = authService;
}
[HttpGet("login")]
public IActionResult Login()
{
var (authUrl, codeVerifier, state) = _authService.CreateLoginRequest();
// Store in session for callback
HttpContext.Session.SetString("oauth_state", state);
HttpContext.Session.SetString("code_verifier", codeVerifier);
return Redirect(authUrl);
}
[HttpGet("callback")]
public async Task<IActionResult> Callback(string code, string state)
{
// Verify state
var expectedState = HttpContext.Session.GetString("oauth_state");
if (state != expectedState)
return BadRequest("Invalid state parameter");
// Exchange code
var codeVerifier = HttpContext.Session.GetString("code_verifier")!;
var token = await _authService.ExchangeCodeAsync(code, codeVerifier);
// Get user profile
using var authenticatedClient = new LichessClient(token.AccessToken);
var profile = await authenticatedClient.Account.GetProfileAsync();
// Clear session data
HttpContext.Session.Remove("oauth_state");
HttpContext.Session.Remove("code_verifier");
// Create application session/cookie with user info
// ... your authentication logic here ...
return RedirectToAction("Index", "Home");
}
[HttpPost("logout")]
public async Task<IActionResult> Logout()
{
var userToken = GetCurrentUserToken();
if (userToken != null)
{
using var client = new LichessClient(userToken);
await client.OAuth.RevokeTokenAsync();
}
// Clear application session
// ... your logout logic here ...
return RedirectToAction("Index", "Home");
}
}-
Always use state parameter - Prevent CSRF attacks by generating a random state, storing it in the session, and verifying it in the callback.
-
Store code verifier securely - The code verifier must be stored server-side (session, database) and never exposed to the client.
-
Use HTTPS in production - OAuth redirect URIs should always use HTTPS in production.
-
Request minimal scopes - Only request the scopes your application actually needs.
-
Store tokens securely - Access tokens are long-lived credentials. Store them encrypted in your database.
-
Handle token expiration - Lichess tokens last approximately one year. Implement token refresh logic by redirecting users through OAuth again when tokens expire.
- Account API - Get user profile after authentication
- Lichess OAuth Documentation