ASP.NET Core and enterprise applications
Turalogin is a hosted passwordless authentication service providing first-class backend support for .NET applications. Start auth to send a one-time login link email, handle the callback when users click the link, and verify the token. Works with ASP.NET Core's built-in authentication system.
Configure your settings in appsettings.json for local development and production.
1 { 2 "Turalogin": { 3 "ApiKey": "your-api-key-here", 4 "ValidationUrl": "http://localhost:5000/auth/callback" 5 } 6 } 7 8 // For production, use environment variables or user secrets: 9 // dotnet user-secrets set "Turalogin:ApiKey" "tl_live_xxx" 10 // dotnet user-secrets set "Turalogin:ValidationUrl" "https://myapp.com/auth/callback"
Create a controller endpoint to initiate authentication. This sends a one-time login link to the user's email.
1 using Microsoft.AspNetCore.Mvc; 2 using System.Text.Json; 3 4 namespace MyApp.Controllers; 5 6 [ApiController] 7 [Route("auth")] 8 public class AuthController : ControllerBase 9 { 10 private readonly TuraloginService _turalogin; 11 12 public AuthController(TuraloginService turalogin) 13 { 14 _turalogin = turalogin; 15 } 16 17 [HttpPost("start")] 18 public async Task<IActionResult> Start([FromBody] StartAuthRequest request) 19 { 20 if (string.IsNullOrEmpty(request.Email)) 21 { 22 return BadRequest(new { error = "Email is required" }); 23 } 24 25 try 26 { 27 await _turalogin.StartAuthAsync(request.Email); 28 return Ok(new { 29 success = true, 30 message = "Check your email for the login link" 31 }); 32 } 33 catch (TuraloginException ex) 34 { 35 return StatusCode(ex.StatusCode, new { error = ex.Message }); 36 } 37 } 38 } 39 40 public record StartAuthRequest(string Email);
Create a callback endpoint that receives the token from the one-time login link and verifies it.
1 [HttpGet("callback")] 2 public async Task<IActionResult> Callback([FromQuery] string token) 3 { 4 if (string.IsNullOrEmpty(token)) 5 { 6 return Redirect("/login?error=invalid_link"); 7 } 8 9 try 10 { 11 var result = await _turalogin.VerifyTokenAsync(token); 12 13 // Create claims for the user 14 var claims = new List<Claim> 15 { 16 new Claim(ClaimTypes.NameIdentifier, result.User.Id), 17 new Claim(ClaimTypes.Email, result.User.Email), 18 }; 19 20 var claimsIdentity = new ClaimsIdentity( 21 claims, 22 CookieAuthenticationDefaults.AuthenticationScheme 23 ); 24 25 var authProperties = new AuthenticationProperties 26 { 27 IsPersistent = true, 28 ExpiresUtc = DateTimeOffset.UtcNow.AddDays(7), 29 }; 30 31 await HttpContext.SignInAsync( 32 CookieAuthenticationDefaults.AuthenticationScheme, 33 new ClaimsPrincipal(claimsIdentity), 34 authProperties 35 ); 36 37 return Redirect("/dashboard"); 38 } 39 catch (TuraloginException) 40 { 41 return Redirect("/login?error=verification_failed"); 42 } 43 } 44 45 [HttpPost("logout")] 46 [Authorize] 47 public async Task<IActionResult> Logout() 48 { 49 await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); 50 return Ok(new { success = true }); 51 }
A service class that handles all Turalogin API interactions.
1 using System.Net.Http.Headers; 2 using System.Text; 3 using System.Text.Json; 4 5 namespace MyApp.Services; 6 7 public class TuraloginService 8 { 9 private readonly HttpClient _httpClient; 10 private readonly string _apiKey; 11 private readonly string _validationUrl; 12 private const string BaseUrl = "https://api.turalogin.com/api/v1"; 13 14 public TuraloginService(HttpClient httpClient, IConfiguration config) 15 { 16 _httpClient = httpClient; 17 _apiKey = config["Turalogin:ApiKey"] 18 ?? throw new ArgumentNullException("Turalogin API key not configured"); 19 _validationUrl = config["Turalogin:ValidationUrl"] 20 ?? throw new ArgumentNullException("Turalogin validation URL not configured"); 21 22 _httpClient.DefaultRequestHeaders.Authorization = 23 new AuthenticationHeaderValue("Bearer", _apiKey); 24 } 25 26 public async Task StartAuthAsync(string email) 27 { 28 await PostAsync("/auth/start", new { 29 email, 30 validationUrl = _validationUrl // Where the one-time login link redirects to 31 }); 32 } 33 34 public async Task<AuthResult> VerifyTokenAsync(string token) 35 { 36 var response = await PostAsync("/auth/verify", new { sessionId = token }); 37 38 var userElement = response.GetProperty("user"); 39 return new AuthResult( 40 Token: response.GetProperty("token").GetString()!, 41 User: new TuraloginUser( 42 Id: userElement.GetProperty("id").GetString()!, 43 Email: userElement.GetProperty("email").GetString()! 44 ) 45 ); 46 } 47 48 private async Task<JsonElement> PostAsync(string endpoint, object data) 49 { 50 var json = JsonSerializer.Serialize(data); 51 var content = new StringContent(json, Encoding.UTF8, "application/json"); 52 53 var response = await _httpClient.PostAsync($"{BaseUrl}{endpoint}", content); 54 var responseBody = await response.Content.ReadAsStringAsync(); 55 var result = JsonDocument.Parse(responseBody).RootElement; 56 57 if (!response.IsSuccessStatusCode) 58 { 59 var error = result.TryGetProperty("error", out var e) 60 ? e.GetString() 61 : "Unknown error"; 62 var code = result.TryGetProperty("code", out var c) 63 ? c.GetString() 64 : "UNKNOWN"; 65 66 throw new TuraloginException(error!, code!, (int)response.StatusCode); 67 } 68 69 return result; 70 } 71 } 72 73 public record AuthResult(string Token, TuraloginUser User); 74 public record TuraloginUser(string Id, string Email);
Custom exception for Turalogin-related errors.
1 namespace MyApp.Exceptions; 2 3 public class TuraloginException : Exception 4 { 5 public string ErrorCode { get; } 6 public int StatusCode { get; } 7 8 public TuraloginException(string message, string code, int status) 9 : base(message) 10 { 11 ErrorCode = code; 12 StatusCode = status; 13 } 14 }
Middleware to handle Turalogin exceptions globally.
1 using System.Text.Json; 2 using MyApp.Exceptions; 3 4 namespace MyApp.Middleware; 5 6 public class TuraloginExceptionMiddleware 7 { 8 private readonly RequestDelegate _next; 9 private readonly ILogger<TuraloginExceptionMiddleware> _logger; 10 11 public TuraloginExceptionMiddleware( 12 RequestDelegate next, 13 ILogger<TuraloginExceptionMiddleware> logger) 14 { 15 _next = next; 16 _logger = logger; 17 } 18 19 public async Task InvokeAsync(HttpContext context) 20 { 21 try 22 { 23 await _next(context); 24 } 25 catch (TuraloginException ex) 26 { 27 _logger.LogWarning( 28 "Turalogin error: {Message} ({Code})", 29 ex.Message, 30 ex.ErrorCode 31 ); 32 33 var message = ex.ErrorCode switch 34 { 35 "INVALID_EMAIL" => "Please provide a valid email address.", 36 "SESSION_EXPIRED" => "Login link has expired. Please try again.", 37 "INVALID_TOKEN" => "Invalid login link.", 38 "RATE_LIMITED" => "Too many attempts. Please wait a moment.", 39 _ => "Authentication error. Please try again." 40 }; 41 42 context.Response.StatusCode = ex.StatusCode; 43 context.Response.ContentType = "application/json"; 44 45 await context.Response.WriteAsync(JsonSerializer.Serialize(new 46 { 47 error = message, 48 code = ex.ErrorCode 49 })); 50 } 51 } 52 } 53 54 // Extension method 55 public static class TuraloginExceptionMiddlewareExtensions 56 { 57 public static IApplicationBuilder UseTuraloginExceptionHandler( 58 this IApplicationBuilder builder) 59 { 60 return builder.UseMiddleware<TuraloginExceptionMiddleware>(); 61 } 62 }
Complete Program.cs setup with one-time login link authentication.
1 using Microsoft.AspNetCore.Authentication.Cookies; 2 using MyApp.Services; 3 using MyApp.Middleware; 4 5 var builder = WebApplication.CreateBuilder(args); 6 7 // Add services 8 builder.Services.AddControllers(); 9 builder.Services.AddHttpClient<TuraloginService>(); 10 11 // Configure authentication 12 builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) 13 .AddCookie(options => 14 { 15 options.Cookie.HttpOnly = true; 16 options.Cookie.SecurePolicy = CookieSecurePolicy.Always; 17 options.Cookie.SameSite = SameSiteMode.Lax; 18 options.ExpireTimeSpan = TimeSpan.FromDays(7); 19 options.SlidingExpiration = true; 20 options.Events.OnRedirectToLogin = context => 21 { 22 context.Response.StatusCode = StatusCodes.Status401Unauthorized; 23 return Task.CompletedTask; 24 }; 25 }); 26 27 builder.Services.AddAuthorization(); 28 29 // CORS (if needed) 30 builder.Services.AddCors(options => 31 { 32 options.AddDefaultPolicy(policy => 33 { 34 policy.WithOrigins(builder.Configuration["FrontendUrl"]!) 35 .AllowCredentials() 36 .AllowAnyMethod() 37 .AllowAnyHeader(); 38 }); 39 }); 40 41 var app = builder.Build(); 42 43 // Middleware pipeline 44 app.UseCors(); 45 app.UseTuraloginExceptionHandler(); 46 app.UseAuthentication(); 47 app.UseAuthorization(); 48 app.MapControllers(); 49 50 app.Run(); 51 52 // appsettings.json 53 /* 54 { 55 "Turalogin": { 56 "ApiKey": "your-api-key-here", 57 "ValidationUrl": "http://localhost:5000/auth/callback" 58 }, 59 "FrontendUrl": "http://localhost:3000" 60 } 61 */ 62 63 // Protected endpoint example 64 [ApiController] 65 [Route("api")] 66 [Authorize] 67 public class ApiController : ControllerBase 68 { 69 [HttpGet("me")] 70 public IActionResult GetMe() 71 { 72 return Ok(new 73 { 74 id = User.FindFirst(ClaimTypes.NameIdentifier)?.Value, 75 email = User.FindFirst(ClaimTypes.Email)?.Value 76 }); 77 } 78 79 [HttpGet("dashboard")] 80 public IActionResult Dashboard() 81 { 82 var email = User.FindFirst(ClaimTypes.Email)?.Value; 83 return Ok(new { message = $"Welcome to the dashboard, {email}!" }); 84 } 85 }