Sign in with HRTG
A standard OpenID Connect provider where every sign-in is a biometric approval on the person's own phone. Your existing library works; there is no SDK to install.
Sign in with HRTG is in beta testing. Access is limited to beta testers: request it before you build, and expect details here to change.
At a glance
- Issuer
https://api.hrtg.me- Discovery
https://api.hrtg.me/.well-known/openid-configuration- Flow
- Authorization code with PKCE (
S256) - Scopes
openidrequired;email,profile- ID token
RS256by default;EdDSAon request- Client authentication
client_secret_basic,client_secret_postorprivate_key_jwt; none with PKCE for apps without a secret- Authorization code
- 60 seconds, single use
- Access token
- 5 minutes, reads
/oidc/userinfoonly
How it works
- 1
Your app sends them to HRTG
A standard authorization code request with PKCE. Your library builds it.
- 2
HRTG asks who is signing in
There is no password and no login cookie at HRTG. Their browser shows a four-character code.
- 3
They approve on their phone
Face ID or a fingerprint, once they have checked the code on the phone matches the one in the browser.
- 4
Your app receives an ID token
Your library exchanges the code and verifies the token. You create your own session.
R4TX
You receive identity, never the vault. The ID token and userinfo carry who the person is, and only the claims you asked for. Nothing the person keeps in their HRTG vault is ever available to your application.
The one practical difference from other providers: the middle step takes as long as a person takes. They have to pick up their phone. A few patterns you may be used to are not available, so start with Confirm HRTG fits your app.
Confirm HRTG fits your app
Read this before you design your session handling.
| Not available | Why | What to do instead |
|---|---|---|
| Silent renewal / hidden-iframe refresh | There is no login cookie for us to check without asking the person | Hold your own session and set its lifetime to suit you |
| Refresh tokens | We only issue authorization codes | Start a new sign-in when your session ends |
| Single sign-on across applications | Every sign-in is a fresh approval on their phone | Expect people to approve once per application |
| Signing someone out everywhere | We have no way to reach your session or anyone else’s | Each application signs out on its own |
If your product depends on silent renewal or on signing people out of everything at once, HRTG is not the right provider for it.
Browser-only apps: the sign-in flow works, but turn off automaticSilentRenew, checkSessionInterval and anything iframe-based. They rely on a capability we do not have and will retry forever.
Not supported at all: implicit flow, hybrid flow, client credentials, password grant, device code.
Request your client
Registration is by request. Send us the details below and we will issue your credentials. A person reviews every registration, because a mistake in a redirect URI is the one thing that can turn a sign-in endpoint into a security hole.
| Detail | What to send |
|---|---|
| Application name | What the person sees when they approve on their phone. Use the name they know your product by |
| Website | Your host, no https:// and no path: acme.example |
| Redirect URIs | Every URI you need, exactly as your library sends it. See Register exact redirect URIs |
| What you need to know about them | Their name, their email address, or neither |
| Where your app runs | In a browser or on a phone: public. On your own server: confidential. For private_key_jwt, include your public key set |
| Related applications | Any other applications that must recognise the same person as one account. Decide now: this cannot be changed once anyone has signed in |
| Logo | Square, at least 192×192. Optional: we show a monogram if you have none |
Email the details to hello@hrtg.me. The link opens a message with the fields ready to fill in.
What you receive
- A client ID, for example
acme-portal. - A client secret, but only if your application runs on your own server. We show it once and never again: we store only a hash of it and cannot recover it. Put it in a secret manager immediately, never in your repository.
Register exact redirect URIs
This is the most common thing to get wrong.
https://acme.example/callback and https://acme.example/callback/ are different URIs. So are …/callback and …/callback?next=/home. Send us the exact string your library will use, trailing slash and all.
- HTTPS is required.
http://localhostandhttp://127.0.0.1are allowed on any port, for local development and for native apps.- Wildcards are never accepted. Register each URI you need, up to ten.
Register your development and production URIs at the same time so you are not blocked later.
Configure your library
Next.js with Auth.js (NextAuth)
// auth.ts
import NextAuth from 'next-auth'
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
{
id: 'hrtg',
name: 'HRTG',
type: 'oidc',
issuer: 'https://api.hrtg.me',
clientId: process.env.HRTG_CLIENT_ID,
clientSecret: process.env.HRTG_CLIENT_SECRET, // omit for a browser-only app
checks: ['pkce', 'state'],
authorization: { params: { scope: 'openid email' } },
},
],
})<button onClick={() => signIn('hrtg')}>Sign in with HRTG</button>Your redirect URI will be https://your-app.example/api/auth/callback/hrtg. Register that exact string.
Any other library
Every conformant library takes the same handful of values. Point it at the discovery URL in At a glance and it configures itself, then set your client ID, your client secret if you have one, your redirect URI, and the scopes you need.
Settings libraries commonly get wrong
PKCE must be on for any app without a secret: browser and mobile apps. Server-side apps holding a secret may leave it off, though we recommend it. If a browser app gets invalid_request at sign-in, this is almost certainly why.
Leave the ID token algorithm as RS256. We also offer EdDSA, but Spring Security and Microsoft’s libraries cannot verify it and fail with an error that does not say so.
Server-side apps choose how they prove who they are. client_secret_basic and client_secret_post both work with your secret. private_key_jwt signs a short assertion with your own key instead (RS256, ES256 or EdDSA), so no shared secret exists at all; send us your public key set at registration. Set the assertion's aud to our issuer, https://api.hrtg.me. The token endpoint URL is accepted too, but the issuer keeps your assertion from being replayed at another server.
Key accounts on sub
| You asked for | You receive |
|---|---|
openid | sub: a stable identifier for this person, at your application |
profile | name |
email | email, and email_verified (always true: we hold no unverified address) |
You also get auth_time: the real moment they approved on their phone, not when the token was issued.
The same person gets a different sub at every application. Yours cannot be matched against anyone else’s records, and theirs cannot be matched against yours. That is deliberate.
For your application it never changes, including when the person replaces their phone or recovers their account.
Store sub. Do not key accounts on the email address. People change their email, and if that is your key you will strand their account.
If you run several applications that need to recognise the same person as one account, say so in your registration (see Request your client). This cannot be changed once anyone has signed in.
Run your own session and sign out locally
Once you have verified the ID token, create your own session and manage it yourself. That is the whole integration.
The access token we issue lasts five minutes and reads exactly one endpoint, the standard /oidc/userinfo. It is not a key to an HRTG API. There isn’t one for applications to call. Read anything you need during sign-in and you are done.
Signing out is local to your application. Clear your session. The person stays signed in to their other applications, and we have no way to reach them.
Give the callback time
Someone has to pick up their phone, so a sign-in takes as long as a person takes. Do not set a short timeout on your callback route.
A sign-in that seems to hang in testing is waiting for someone to approve on a phone.
Before you go live
When something goes wrong
Our token endpoint returns the same invalid_grant for every problem with a code: wrong, expired, reused, or sent with a verifier or redirect URI that does not match. Telling them apart would help an attacker guess, so the message will not narrow it down. A bad or missing secret is reported separately, as invalid_client. Work through this instead:
| What you see | Almost always |
|---|---|
| An error page on HRTG’s own site, no redirect back | Your client ID is unknown, or the redirect URI does not match exactly. We will not redirect to a URI we cannot verify |
invalid_grant when exchanging the code | The code_verifier does not match the sign-in (or was sent for a sign-in that had no PKCE); or the redirect URI you sent when exchanging differs from the one you sent at sign-in; or the code is more than 60 seconds old |
invalid_client | Wrong secret, or a server-side app sending no secret at all |
login_required, immediately | You sent prompt=none. It can never succeed here |
invalid_scope | You left out openid. Other scopes your application is not registered for are dropped rather than refused; the token response’s scope lists what you were given |
| Sign-in “hangs” in testing | Someone has to approve on a phone. It is not hung |
Codes expire in 60 seconds and work once. Presenting one twice looks exactly like an attack, so we revoke the session immediately. If an exchange fails, start a new sign-in rather than retrying with the same code.
Getting help
Contact us at hello@hrtg.me with your client ID and roughly when the problem happened. For changes to a registration, the same route applies: a new redirect URI, a new scope, a rotated secret.