Skip to content
Orvanov0.2.0

.NET quickstart

You build an ASP.NET Core Minimal API whose GET /me reads a user’s access token, checks it with the Orvano NuGet package 0.2.0, and answers with that user. You also write a command that creates a user with a server API key. This is the pattern for any backend of yours that sits next to an app signing users in with Orvano. It takes about ten minutes.

The finished app is examples/dotnet-quickstart in the Orvano repo. Every file below comes from it, and CI runs it on every change.

If Orvano isn’t running on your machine yet, run these three commands. Run Orvano locally explains each one.

Terminal window
mkdir orvano-local && cd orvano-local
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/install" ghcr.io/orvanohq/orvano:0.2.0 install --local --yes
docker compose up -d --wait

You should see every service report Healthy, and the console open at http://localhost:7700.

  1. Open the setup link the installer printed and create the first admin. You land in your personal org.

    The setup page with a name, email, and password filled in, above the Create the first admin button.The setup page with a name, email, and password filled in, above the Create the first admin button.
  2. Select Create project, name it, and select Create project again. Setting it up takes a few seconds.

    The Create a project dialog with the name My app.The Create a project dialog with the name My app.
  3. On the project’s overview, copy the Project ID.

    The project overview, showing the project ID with a copy button.The project overview, showing the project ID with a copy button.
  4. Open API keys and select Create key. Name it, tick the scopes Users read and Users write, and select Create key. Copy the key it shows: you only see it once. A key can do only what its scopes allow, so this one can read and create users and nothing else.

    The Create an API key dialog with the name Quickstart server and the scopes Users read and Users write ticked.The Create an API key dialog with the name Quickstart server and the scopes Users read and Users write ticked.

You need the .NET 10 SDK. Make a folder with this project file, named DotnetQuickstart.csproj. It pins the Orvano package to 0.2.0, the version of the Orvano you’re running.

DotnetQuickstart.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Orvano" Version="0.2.0" />
</ItemGroup>
</Project>

Every piece of code below goes in one Program.cs, in the order shown.

The server reads its settings from the environment, so the API key never sits in your code or your repo. Set them in the terminal you run the server from, with your project ID and the key you copied:

Terminal window
export ORVANO_ENDPOINT=http://localhost:7700
export ORVANO_PROJECT=your-project-id
export ORVANO_API_KEY=your-api-key

Make one client for the server. It sends the API key with every call, and it checks access tokens against your project’s public keys, which it caches for 10 minutes:

Program.cs
// The server client. It sends your API key with every call and checks your users' access tokens against the
// project's public keys. Settings come from the environment, so the key never sits in code.
var endpoint = new Uri(Setting("ORVANO_ENDPOINT", "http://localhost:7700"));
using var orvano = new OrvanoClient(new OrvanoClientOptions(endpoint)
{
Project = Setting("ORVANO_PROJECT"),
ApiKey = Setting("ORVANO_API_KEY"),
});

On a server, you create users with the API key, for example when you import them from another system. dotnet run -- create-user creates one and exits before the web server starts; it runs on your machine and is never an open HTTP route:

Program.cs
// `dotnet run -- create-user <email> <password> [name]` creates a user with the API key, then exits.
if (args is ["create-user", var email, var password, ..])
{
try
{
var created = await orvano.Users.CreateAsync(
new CreateUserRequest(email, password, Name: args.Length > 3 ? args[3] : null));
Console.WriteLine($"Created user {created.Id} ({created.Email}).");
return 0;
}
catch (OrvanoException error)
{
Console.Error.WriteLine($"Orvano refused: {error.Message} ({error.Code})");
return 1;
}
}

Run it:

Terminal window
dotnet run -- create-user ada@example.com 'a long password' 'Ada Lovelace'

You should see Created user with the new user’s ID. The user is in the console too, on your project’s Users page.

GET /me reads the token from Authorization: Bearer <token>. VerifyAccessTokenAsync checks its signature, its project, and its expiry, and answers with the user’s ID, so the server loads the user with Users.GetAsync. A missing or bad token gets 401.

Program.cs
// GET /me checks the access token in `Authorization: Bearer <token>`, then answers with the user it belongs to.
// A missing or bad token gets 401.
app.MapGet("/me", async (HttpContext context, OrvanoClient orvano, CancellationToken cancellationToken) =>
{
var header = context.Request.Headers.Authorization.ToString();
if (!header.StartsWith("Bearer ", StringComparison.Ordinal)) return Unauthorized(context);
try
{
// Checks the signature, the project, and the expiry, without a call to Orvano once the keys are cached.
var token = await orvano.VerifyAccessTokenAsync(
header["Bearer ".Length..], cancellationToken: cancellationToken);
var user = await orvano.Users.GetAsync(token.UserId, cancellationToken);
return Results.Ok(new { user.Id, user.Email, user.Name });
}
catch (OrvanoException error) when (error.Status == StatusCodes.Status401Unauthorized)
{
// invalid_token or token_expired; anything else is a real failure.
return Unauthorized(context);
}
});
static IResult Unauthorized(HttpContext context)
{
context.Response.Headers.WWWAuthenticate = "Bearer";
return Results.Json(
new { error = "Send a valid access token." }, statusCode: StatusCodes.Status401Unauthorized);
}
The whole Program.cs, with the web host and the settings helper
Program.cs
using Orvano;
// The server client. It sends your API key with every call and checks your users' access tokens against the
// project's public keys. Settings come from the environment, so the key never sits in code.
var endpoint = new Uri(Setting("ORVANO_ENDPOINT", "http://localhost:7700"));
using var orvano = new OrvanoClient(new OrvanoClientOptions(endpoint)
{
Project = Setting("ORVANO_PROJECT"),
ApiKey = Setting("ORVANO_API_KEY"),
});
// `dotnet run -- create-user <email> <password> [name]` creates a user with the API key, then exits.
if (args is ["create-user", var email, var password, ..])
{
try
{
var created = await orvano.Users.CreateAsync(
new CreateUserRequest(email, password, Name: args.Length > 3 ? args[3] : null));
Console.WriteLine($"Created user {created.Id} ({created.Email}).");
return 0;
}
catch (OrvanoException error)
{
Console.Error.WriteLine($"Orvano refused: {error.Message} ({error.Code})");
return 1;
}
}
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://localhost:3002");
builder.Services.AddSingleton(orvano);
var app = builder.Build();
// GET /me checks the access token in `Authorization: Bearer <token>`, then answers with the user it belongs to.
// A missing or bad token gets 401.
app.MapGet("/me", async (HttpContext context, OrvanoClient orvano, CancellationToken cancellationToken) =>
{
var header = context.Request.Headers.Authorization.ToString();
if (!header.StartsWith("Bearer ", StringComparison.Ordinal)) return Unauthorized(context);
try
{
// Checks the signature, the project, and the expiry, without a call to Orvano once the keys are cached.
var token = await orvano.VerifyAccessTokenAsync(
header["Bearer ".Length..], cancellationToken: cancellationToken);
var user = await orvano.Users.GetAsync(token.UserId, cancellationToken);
return Results.Ok(new { user.Id, user.Email, user.Name });
}
catch (OrvanoException error) when (error.Status == StatusCodes.Status401Unauthorized)
{
// invalid_token or token_expired; anything else is a real failure.
return Unauthorized(context);
}
});
static IResult Unauthorized(HttpContext context)
{
context.Response.Headers.WWWAuthenticate = "Bearer";
return Results.Json(
new { error = "Send a valid access token." }, statusCode: StatusCodes.Status401Unauthorized);
}
app.Run();
return 0;
static string Setting(string name, string? fallback = null)
{
var value = Environment.GetEnvironmentVariable(name) ?? fallback;
return string.IsNullOrEmpty(value)
? throw new InvalidOperationException($"Set the {name} environment variable.")
: value;
}

Start the server with dotnet run. You should see Now listening on: http://localhost:3002.

In another terminal (with the same settings), sign in as the user you created to get an access token, the way an app’s sign in would, and call /me with it:

Terminal window
curl -s http://localhost:7700/v1/account/sessions/password \
-H "X-Orvano-Project: $ORVANO_PROJECT" -H "Content-Type: application/json" \
-d '{"email":"ada@example.com","password":"a long password"}'
# Copy session.accessToken from the answer, then:
TOKEN=paste-the-access-token-here
curl -i http://localhost:3002/me -H "Authorization: Bearer $TOKEN"
  1. You should see 200 OK and the user’s ID, email, and name.
  2. Call /me again without the Authorization header. You should see 401 Unauthorized.
  3. Change one character of the token and call it again. You should see 401 again: the signature no longer matches.

Access tokens last 15 minutes. In a real app, the client SDK refreshes the session and sends a fresh token with each call.