Skip to content

.NET

Package: Tenon.Auth 1.0.0.

  • Targets: .NET 8+, and .NET Standard 2.0 for .NET Framework 4.7.2+.
  • Dependencies: none on .NET 8+; System.Text.Json on .NET Standard 2.0.
  • Thread safety: the client is thread-safe; register it as a singleton.

Download the package from Downloads, check its SHA-256, put it in a folder, and add it as a local source:

Terminal window
dotnet nuget add source ./packages --name local
dotnet add package Tenon.Auth --version 1.0.0
builder.Services.AddSingleton(new TenonAuthClient(new TenonAuthClientOptions
{
BaseUrl = builder.Configuration["TenonAuth:Url"], // https://api.glow-kin.com
ApiKey = builder.Configuration["TenonAuth:ApiKey"], // user secrets, Key Vault
}));
// The visitor's IP address behind your load balancer:
builder.Services.Configure<ForwardedHeadersOptions>(o => o.ForwardedHeaders = ForwardedHeaders.XForwardedFor);

Options:

  • Timeout (30 s) and ConnectTimeout (5 s);
  • Headers;
  • your own HttpClient: new TenonAuthClient(options, httpClient), e.g. from IHttpClientFactory, with redirects off.
[HttpPost("/login")]
public async Task<IActionResult> Login(LoginForm form)
{
var account = await _accounts.CheckPasswordAsync(form.Username, form.Password); // yours
SecondStep step;
try
{
step = await _tenonAuth.BeginSecondStepAsync(account.Id.ToString(),
new SignInContext(HttpContext.Connection.RemoteIpAddress?.ToString(), Request.Headers.UserAgent),
Request.Cookies["tenon_device"]);
}
catch (TenonAuthUnavailableException)
{
return EmergencyOrRefuse(account); // fail closed
}
HttpContext.Session.SetString("SecondStep", JsonSerializer.Serialize(step));
return step.Kind switch
{
SecondStepKind.CodeEntry => Redirect("/login/code"),
SecondStepKind.PushWait => Redirect("/login/approve"),
SecondStepKind.RecoveryOnly => Redirect("/login/recover"),
_ => await SignInAsync(account), // Trusted or NoMethod
};
}
[HttpPost("/login/code")]
public async Task<IActionResult> Code(string code)
{
var step = JsonSerializer.Deserialize<SecondStep>(HttpContext.Session.GetString("SecondStep")!)!;
var result = await _tenonAuth.CompleteWithCodeAsync(step, code);
if (result.Approved) return await SignInAsync(step.UserId);
if (result.Status == SecondStepStatus.WrongCode) return Redirect($"/login/code?left={result.AttemptsRemaining}");
return Redirect("/login"); // Failed, Expired
}

The waiting page calls PollApprovalAsync(step) every 2 seconds. The recovery page calls CompleteWithRecoveryCodeAsync(step, code).

All errors are TenonAuthException subclasses:

  • TenonAuthUnavailableException (Reason);
  • ApiKeyRejectedException, ForbiddenException;
  • InvalidRequestException, NotFoundException, InvalidCodeException;
  • FactorLockedException, TooManyRequestsException;
  • EmailRefusedException, EmailUnavailableException;
  • ConflictException.

See Errors.