Skip to content

Provider Selection Pages

When multiple identity providers are configured, AuthProxy serves a provider-selection page so the user can choose which provider to sign in with. Two pages serve this role depending on the flow that triggered them:

PageTriggered by
select-provider.htmlDirect navigation to a protected resource when not yet authenticated.
invitation-select-provider.htmlFollowing a valid invite link.

Both pages work identically — they receive provider data via the .cratis-providers cookie and can be overridden in the same way. The built-in versions are functional but minimally styled. This guide explains how to replace either or both with fully branded custom pages.


Before serving either page, AuthProxy sets a short-lived, non-HTTP-only cookie named .cratis-providers. The value is a URL-encoded JSON array — one entry per configured identity provider:

[
{
"name": "Contoso AD",
"type": "Microsoft",
"loginUrl": "/.cratis/login/contoso-ad"
},
{
"name": "GitHub",
"type": "GitHub",
"loginUrl": "/.cratis/login/github"
}
]
FieldTypeDescription
namestringHuman-readable display name taken from Authentication:OidcProviders[].Name.
typestringProvider brand — Microsoft, Google, GitHub, Apple, or Custom. Use this to pick logos or apply brand-specific styling.
loginUrlstringAuthProxy-relative URL that initiates the OIDC/OAuth challenge for this provider. Navigating to this URL starts the login flow and, after a successful login, redirects the user back to the original destination.

The cookie expires after 15 minutes and is deleted automatically by the browser after that time.


The cookie is not HTTP-only so your page’s JavaScript can access it directly via document.cookie. The value is URL-encoded, so decode it before parsing:

/**
* Returns the decoded value of the named cookie, or null if it is not present.
*/
function getCookie(name) {
var pattern = new RegExp('(?:^|; )' +
name.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1') + '=([^;]*)');
var match = document.cookie.match(pattern);
return match ? decodeURIComponent(match[1]) : null;
}
var providersJson = getCookie('.cratis-providers');
var providers = providersJson ? JSON.parse(providersJson) : [];

A custom page only needs to:

  1. Read the .cratis-providers cookie.
  2. Render a clickable sign-in element whose href is the provider’s loginUrl.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sign In</title>
<link rel="stylesheet" href="/_pages/brand.css" />
</head>
<body>
<main id="content">
<h1>Choose how you want to sign in</h1>
<ul id="providers"></ul>
<p id="error" hidden>
Unable to load sign-in options.
Please try again or contact support.
</p>
</main>
<script>
(function () {
function getCookie(name) {
var pattern = new RegExp('(?:^|; )' +
name.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1') + '=([^;]*)');
var match = document.cookie.match(pattern);
return match ? decodeURIComponent(match[1]) : null;
}
var json = getCookie('.cratis-providers');
var providers;
try { providers = JSON.parse(json); } catch (_) { providers = null; }
if (!providers || providers.length === 0) {
document.getElementById('error').hidden = false;
return;
}
var list = document.getElementById('providers');
providers.forEach(function (provider) {
var li = document.createElement('li');
var a = document.createElement('a');
a.href = provider.loginUrl;
a.textContent = 'Sign in with ' + provider.name;
a.className = 'provider-btn provider-btn--' + provider.type.toLowerCase();
li.appendChild(a);
list.appendChild(li);
});
}());
</script>
</body>
</html>

Note: Assets placed in the same pages directory are served at the /_pages/ URL prefix. The brand.css reference above assumes brand.css lives alongside your custom page.

The same HTML template works for both select-provider.html and invitation-select-provider.html. You can use identical files, or tailor each one — for example, showing “You have been invited” in the invitation variant and “Sign in to continue” in the direct-access variant.


  1. Create the HTML file(s) named exactly select-provider.html and/or invitation-select-provider.html.
  2. Place any CSS, images, or other assets in the same directory.
  3. Mount the directory into your container and point Cratis:AuthProxy:PagesPath at the mount path:
docker-compose.yml
services:
authproxy:
image: cratis/authproxy:latest
volumes:
- ./my-pages:/mnt/pages
environment:
Cratis__AuthProxy__PagesPath: /mnt/pages

AuthProxy resolves each page file by name — if a file exists in the configured PagesPath it is used; otherwise the built-in default is served.


The type value maps to the OidcProviderType enum. Use it to apply provider-specific logos or colors:

var LOGOS = {
Microsoft: '/_pages/icons/microsoft.svg',
Google: '/_pages/icons/google.svg',
GitHub: '/_pages/icons/github.svg',
Apple: '/_pages/icons/apple.svg',
Custom: '/_pages/icons/generic.svg'
};
providers.forEach(function (provider) {
var a = document.createElement('a');
a.href = provider.loginUrl;
var img = document.createElement('img');
img.src = LOGOS[provider.type] || LOGOS.Custom;
img.alt = provider.name;
img.width = 20;
img.height = 20;
a.appendChild(img);
a.appendChild(document.createTextNode(' Sign in with ' + provider.name));
document.getElementById('providers').appendChild(a);
});

User navigates to protected resource
Unauthenticated request detected
├─ Single provider ──► redirect to /.cratis/login/<scheme>
└─ Multiple providers
Set .cratis-providers cookie
Serve select-provider.html
│ User clicks provider
GET /.cratis/login/<scheme>
OIDC/OAuth challenge
Redirect to original resource

Invitation flow (invitation-select-provider.html)

Section titled “Invitation flow (invitation-select-provider.html)”
User clicks invite link
GET /invite/<token>
├─ Token invalid/expired ──► invitation-invalid.html / invitation-expired.html
└─ Token valid
├─ Single provider, not signed in ──► redirect to /.cratis/login/<scheme>
└─ Multiple providers, or already signed in
Set .cratis-providers cookie
Serve invitation-select-provider.html
│ User clicks provider
GET /.cratis/login/<scheme>
OIDC/OAuth challenge
POST-login exchange
Redirect to lobby / application

An invitation is completed only with the identity that authenticated for that invitation. A session the browser was already carrying — from an earlier sign-in, possibly with a different provider — never completes one, even though it is a perfectly valid session: the organization would be bound permanently to an account the person did not choose for it.

Such a caller is therefore always shown invitation-select-provider.html, even where only one provider is configured, and the invitation completes with whatever identity comes back from the provider they pick. Picking the provider they are already signed in with normally costs no more than a redirect, because the identity provider still has its own session.

The pending invitation is left in place while this happens, so nothing is lost — it stays valid for the lifetime of the .cratis-invite cookie.