Authentication
Skiff 2 provisions Google Identity-Aware Proxy (IAP) in front of every service by default. You don't need to configure anything to get authentication — by default, only users with an @allenai.org account can reach your app.
To grant access to other domains, specific users, or Google Groups while still requiring login, see Allowing other domains, users, or groups.
For public-facing apps, set allowUnauthenticated: true on the service in skiff2.json:
{
"services": [
{
"name": "web",
"allowUnauthenticated": true
}
]
}
See the skiff2.json reference for all available options.
For more on how IAP works and how to manage access, see the Google IAP documentation.
IAP itself is all-or-nothing per service — you can't configure it to require auth on some paths and not others. To mix public and private pages on the same service, see Mixing public and private pages below.
Allowing other domains, users, or groups
By default, only @allenai.org accounts can reach your service. To grant access to additional Google Workspace domains, individual users, or Google Groups, set allowedPrincipals on the service. IAP still forces a Google sign-in — this list controls who passes the check after they've signed in.
{
"services": [
{
"name": "web",
"allowedPrincipals": [
"domain:allenai.org",
"user:contractor@example.com",
"group:partners@allenai.org"
]
}
]
}
allowedPrincipals defaults to ["domain:allenai.org"], so if you set it you'll usually want to keep domain:allenai.org in the list alongside your additions.
Each entry is a literal GCP IAM member identifier:
| Form | Example | Meaning |
|---|---|---|
domain: | domain:example.com | Anyone in this Google Workspace domain |
user: | user:alice@example.com | A specific user |
group: | group:team@allenai.org | A Google Group |
allAuthenticatedUsers | allAuthenticatedUsers | Anyone signed in with any Google account, including personal @gmail.com |
allAuthenticatedUsers is broader than it soundsallAuthenticatedUsers lets in any Google account on the internet — not just AI2 users. In most cases you want domain:allenai.org plus specific user: or group: exceptions instead.
allAuthenticatedUsers needs a one-time manual OAuth setupIAP's default Google-managed OAuth client only lets in accounts inside the AI2 org, so allAuthenticatedUsers won't work until you switch the service to a custom OAuth client. Terraform can't do this for you — it's two quick steps in the GCP console:
- Configure the OAuth consent screen. Open the Google Auth overview for your project and fill out the branding form. It's quick — mainly an app name and a support contact email.
- Switch IAP to custom OAuth. Open Identity-Aware Proxy, find your service in the list, click the ⋮ menu → Settings, and change Google-Managed OAuth (recommended for simplicity) to Custom OAuth (for specific control, branding, or external users).
Add ?project=YOUR_PROJECT_ID to either console link to jump straight to your project. Once both are done, external users can sign in.
allowedPrincipals is ignored when allowUnauthenticated: true (the service is fully public in that case). To make a service public, use allowUnauthenticated, not "allUsers" here.
See the skiff2.json reference for the full field spec.
Identifying the user
When a request reaches your service through IAP, the authenticated user's email is available on the X-Goog-Authenticated-User-Email request header.
The header value is prefixed with an identity namespace (e.g. accounts.google.com:user@allenai.org). You'll typically want to strip everything up to and including the first : to get the bare email address.
The X-Goog-Authenticated-User-Email header is only trustworthy when IAP is enforcing auth on the service. If the service has allowUnauthenticated: true (including mixed public/private services), IAP forwards requests without authenticating, and a caller can set this header to any value. In that case, verify the signed X-Goog-IAP-JWT-Assertion header instead — see Verifying the user on mixed services.
Mixing public and private pages
IAP can't gate some paths and leave others open on the same service, but you can get the same effect at the application layer:
- Set
allowUnauthenticated: trueon the service so every request reaches your app. - In your handler, verify the
X-Goog-IAP-JWT-Assertionheader to identify the user (see Verifying the user on mixed services below). - If the request is for a private page and the JWT is missing or invalid, redirect the user to the same URL with
?gcp-iap-mode=FORCE_LOGINappended. IAP intercepts this parameter, walks the user through Google sign-in, and redirects them back to the original URL with a valid JWT attached.
X-Goog-Authenticated-User-Email on a mixed serviceWhen allowUnauthenticated: true is set, IAP forwards requests without authenticating them, so a caller can send their own X-Goog-Authenticated-User-Email header and your app will see it. On a fully private service that header is safe because IAP strips and overwrites it; on a mixed service it is not. Always validate the IAP JWT instead — see below.
Verifying the user on mixed services
On a mixed service, the only trustworthy signal of the caller's identity is the X-Goog-IAP-JWT-Assertion header. It's a signed JWT that IAP attaches to any request where the user has already authenticated. Your app verifies it and reads the user's email from the claims.
A valid verification checks four things:
- Signature — verify the JWT against IAP's public keys, published at Google's IAP public key endpoint. Google's official client libraries fetch and cache these for you.
- Issuer — must be
https://cloud.google.com/iap. - Audience — must match your service exactly, in the form
/projects/PROJECT_NUMBER/locations/REGION/services/SERVICE_NAME. FindPROJECT_NUMBERin the GCP console for your project. - Email claim — after the JWT is verified, read the
emailclaim and apply whatever access rules your app needs (e.g. require an@allenai.orgsuffix).
If any step fails, treat the request as unauthenticated.
For development, it's common to bypass verification behind an environment variable (e.g. LOCAL_DEVELOPMENT) and return a stub email, since IAP isn't in front of your local server.
Google's Getting the user's identity with IAP docs have sample code in several languages and the full list of claims available on the JWT.
How force-login works
When your service has allowUnauthenticated: true, IAP won't challenge visitors on its own. To trigger a sign-in from your app, redirect the user to their current URL with the gcp-iap-mode=FORCE_LOGIN query parameter:
https://my-app.pandajungle.org/admin?gcp-iap-mode=FORCE_LOGIN
IAP intercepts requests containing this parameter, enforces authentication, and then redirects the user back to the URL without the parameter. On that subsequent request, the X-Goog-Authenticated-User-Email header will be present.
This keeps the auth decision in your code — useful when "is this page public?" depends on the route, a feature flag, or the resource being requested.
See Google's IAP as an authentication proxy docs for more details.
Need something more complicated? Use Auth0
IAP covers the common case (gate the app to @allenai.org, or make it fully public). If you need anything beyond that — external users, per-user roles, organizations, SSO into a non-Google IdP, machine-to-machine tokens, etc. — AI2 runs an Auth0 tenant you can use instead.
Ask in #auth0-users on Slack to get onboarded. You'll typically run your service with allowUnauthenticated: true and let Auth0 handle auth inside your app.