How to Add “Sign in with Google” to Your Quarkus App
Implement OIDC authentication, automatic tenant detection, and Google API access — step by step for Java developers.
Quarkus makes OIDC simple. In this hands-on tutorial you’ll build a small web app where users sign in with Google, and your app auto-creates a tenant workspace based on their email domain.
The result is a clean pattern you can reuse for SaaS: sign in once, derive tenant from identity, and use the authorized access levels.
Prerequisites and versions
Java 21, Maven 3.9+
A Google Cloud project with OAuth 2.0 Client ID (Web application)
Optional: Podman (or Docker) if you prefer containers later
Follow Quarkus’ OIDC “code flow” guide for background on how the web-app flow works, the redirect_uri setting, and scopes. Quarkus has first-class docs for this and for well-known providers like Google.
Google’s official identity docs explain creating OAuth credentials and using them to call Google APIs. We’ll use the openid, email, and profile and calendar.readonly.
For token propagation to Google APIs from your Quarkus app, see Quarkus’ OIDC client + token propagation guides. We’ll use the REST Client Token Propagation filter to forward the user’s access token.
Bootstrap the project
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=com.acme \
-DprojectArtifactId=google-oidc-saas \
-DclassName="com.acme.web.HomeResource" \
-Dpath="/" \
-Dextensions="rest,rest-qute,oidc,qute,rest-client,rest-client-oidc-token-propagation,quarkus-rest-client-oidc-filter,hibernate-orm-panache,hibernate-validator,jdbc-h2"
cd google-oidc-saasWhy these matter
quarkus-oidcturns on the Authorization Code flow for browser login.qute, qute-restrenders the login and dashboard pages.rest-client+rest-client-oidc-token-propagation + quarkus-rest-client-oidc-filterforwards the current user’s Google access token to Google APIs.Panache + H2 stores minimal user/tenant data.
Docs: authorization-code web-app, token propagation, config reference. (quarkus.io)
Configure Google OIDC
Create OAuth credentials in Google Cloud Console:
Application type: Web application
Authorized redirect URI:
http://localhost:8080/q/oidc/callback
(Quarkus can also use a custom relative path viaquarkus.oidc.authentication.redirect-path; the default callback works fine for local dev.)
Store secrets as environment variables (never commit them):
export OIDC_GOOGLE_CLIENT_ID=”YOUR_GOOGLE_CLIENT_ID”
export OIDC_GOOGLE_CLIENT_SECRET=”YOUR_GOOGLE_CLIENT_SECRET”src/main/resources/application.properties:
# Quarkus OIDC: Google as the provider
# Quarkus knows how to talk to well-known providers; set provider=google.
quarkus.oidc.provider=google
quarkus.oidc.client-id=${OIDC_GOOGLE_CLIENT_ID}
quarkus.oidc.credentials.secret=${OIDC_GOOGLE_CLIENT_SECRET}
quarkus.oidc.application-type=web-app
# Ask for OpenID, email, profile, and calendar on the initial login.
quarkus.oidc.authentication.scopes=openid,email,profile,https://www.googleapis.com/auth/calendar.readonly
# Set the redirect URI to the standard OIDC callback path
quarkus.oidc.authentication.redirect-path=/q/oidc/callback
# Restore original path after callback and clean code/state params
quarkus.oidc.authentication.restore-path-after-redirect=true
quarkus.oidc.authentication.remove-redirect-parameters=true
# Make Quarkus call Google UserInfo endpoint so we can rely on enriched claims
quarkus.oidc.authentication.user-info-required=true
# Minimal security: everything requires auth except assets and the landing page
quarkus.http.auth.permission.public.paths=/assets/*,/
quarkus.http.auth.permission.public.policy=permit
quarkus.http.auth.permission.app.paths=/*
quarkus.http.auth.permission.app.policy=authenticated
# Dev DB
quarkus.datasource.db-kind=h2
quarkus.datasource.jdbc.url=jdbc:h2:mem:saas;DB_CLOSE_DELAY=-1
quarkus.hibernate-orm.schema-management.strategy = drop-and-create
# REST Client base URLs for Google APIs
# People API (profile alias), Calendar API
quarkus.rest-client.google-people.url=https://people.googleapis.com
quarkus.rest-client.google-calendar.url=https://www.googleapis.comNotes
provider=googlelets Quarkus discoverissuer,authorization_uri,token_uri, anduserinfo_uriautomatically, including Google’shttps://openidconnect.googleapis.com/v1/userinfo.The
authentication.scopesproperty is the supported way to ask for more scopes with the web-app flow.
Domain model: tenants by email domain
We’ll derive a Tenant from the email domain (e.g., acme.com), and store User and Tenant rows on first login.
src/main/java/com/acme/tenant/Tenant.java
package com.acme.tenant;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@Entity
public class Tenant extends PanacheEntity {
@Column(unique = true, nullable = false)
public String domain;
public static Tenant getOrCreate(String domain) {
Tenant t = find(”domain”, domain).firstResult();
if (t == null) {
t = new Tenant();
t.domain = domain.toLowerCase();
t.persist();
}
return t;
}
}src/main/java/com/acme/tenant/AppUser.java
package com.acme.tenant;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = “app_user”)
public class AppUser extends PanacheEntity {
@Column(unique = true, nullable = false)
public String email;
public String name;
public String picture;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
public Tenant tenant;
public static AppUser upsert(String email, String name, String picture, Tenant tenant) {
AppUser u = find(”email”, email.toLowerCase()).firstResult();
if (u == null) {
u = new AppUser();
u.email = email.toLowerCase();
}
u.name = name;
u.picture = picture;
u.tenant = tenant;
u.persist();
return u;
}
}Simple UI with Qute
src/main/resources/templates/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset=”utf-8”/>
<title>Quarkus Google OIDC SaaS</title>
</head>
<body>
<h1>Multi-Tenant Dashboard (Google Login)</h1>
<p><a href=”/dashboard”>Login with Google</a></p>
</body>
</html>src/main/resources/templates/dashboard.html
<!DOCTYPE html>
<html>
<head>
<meta charset=”utf-8” />
<title>Dashboard</title>
</head>
<body>
<h2>Welcome {userName} (@{userEmail})</h2>
{#if userPicture}
<img src=”{userPicture}” alt=”avatar” width=”64” height=”64” />
{/if}
<p>Tenant: {tenantDomain}</p>
<h3>Calendar</h3>
<p>
<a href=”/calendar/upcoming”>Show my next events</a>
</p>
<p><a href=”/logout”>Logout</a></p>
</body>
</html>Core implementation
Web Layer with JWT Token Access
src/main/java/com/acme/web/HomeResource.java
package com.acme.web;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Path(”/”)
public class HomeResource {
@Inject
Template index;
@GET
public TemplateInstance home() {
return index.instance();
}
}src/main/java/com/acme/web/DashboardResource.java
package com.acme.web;
import org.eclipse.microprofile.jwt.JsonWebToken;
import com.acme.tenant.AppUser;
import com.acme.tenant.Tenant;
import io.quarkus.oidc.IdToken;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Path(”/dashboard”)
@RequestScoped
public class DashboardResource {
@Inject
Template dashboard;
@Inject
@IdToken
JsonWebToken idToken;
private static String domainOf(String email) {
int at = email.indexOf(’@’);
return at > 0 ? email.substring(at + 1).toLowerCase() : “unknown”;
}
@GET
@Transactional
public TemplateInstance view() {
String userEmail = idToken.getClaim(”email”);
String userName = idToken.getClaim(”preferred_username”);
String userPicture = idToken.getClaim(”picture”);
Tenant tenant = Tenant.getOrCreate(domainOf(userEmail));
AppUser.upsert(userEmail, userName, userPicture, tenant);
return dashboard.instance()
.data(”userEmail”, userEmail)
.data(”userName”, userName)
.data(”userPicture”, userPicture)
.data(”tenantDomain”, tenant.domain);
}
}Uses @IdToken to access JWT claims from Google
Automatically creates/updates tenant and user records
Passes data to Qute template for rendering
Google Calendar API Integration
We’ll fetch the user’s next events from their primary calendar.
src/main/java/com/acme/google/CalendarClient.java
package com.acme.google;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.common.AccessToken;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
@Path(”/calendar/v3”)
@RegisterRestClient(configKey = “google-calendar”)
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@AccessToken
// @RegisterProvider(GoogleAccessTokenFilter.class)
public interface CalendarClient {
@GET
@Path(”/calendars/primary/events”)
String listPrimaryEvents(@QueryParam(”maxResults”) @DefaultValue(”5”) int max,
@QueryParam(”singleEvents”) @DefaultValue(”true”) boolean singleEvents,
@QueryParam(”orderBy”) @DefaultValue(”startTime”) String orderBy,
@QueryParam(”timeMin”) String timeMinIso);
}@AccessToken annotation automatically propagates OAuth token
REST Client handles HTTP communication with Google APIs
Returns upcoming calendar events as JSON
Because we added rest-client-oidc-token-propagation, Quarkus automatically adds the Authorization: Bearer <user_access_token> header for the current user’s session when this client is invoked.
src/main/java/com/acme/web/CalendarResource.java
package com.acme.web;
import java.time.OffsetDateTime;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import com.acme.google.CalendarClient;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path(”/calendar”)
public class CalendarResource {
@Inject
@RestClient
CalendarClient calendar;
@GET
@Path(”/upcoming”)
@Produces(MediaType.APPLICATION_JSON)
public String upcoming() {
String timeMin = OffsetDateTime.now().minusMinutes(1).toString();
return calendar.listPrimaryEvents(5, true, “startTime”, timeMin);
}
}Google API references for People and Calendar endpoints and scopes. We’re using Calendar’s events REST endpoint; you can swap in People API if you want richer profiles.
Logout
src/main/java/com/acme/web/LogoutResource.java
package com.acme.web;
import io.quarkus.oidc.OidcSession;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;
@Path(”/logout”)
public class LogoutResource {
@Inject
OidcSession oidcSession;
@GET
public Response logout() {
// Use OidcSession for local logout - this will clear the local session
// and redirect to the home page
oidcSession.logout().await().indefinitely();
return Response.seeOther(java.net.URI.create(”/”)).build();
}
}Build, run, and verify
Start the app:
./mvnw quarkus:devVerify the flow end-to-end:
Open
http://localhost:8080and click “Login with Google”.
Consent to
openid email profile, calendar. You return to/dashboardshowing your name, email, avatar, and derived tenant (email domain).Call
http://localhost:8080/calendar/upcoming. You should get a JSON payload with up to 5 events.
Expected snippet
{
“kind”: “calendar#events”,
“items”: [
{ “summary”:”Team Sync”, “start”: { “dateTime”: “...” }, “end”: { “dateTime”: “...” } }
]
}
The Complete Flow
Production notes
HTTPS and proxies. If you run behind a reverse proxy or different external host, set
quarkus.oidc.authentication.redirect-pathappropriately soredirect_urimatches what Google expects. The docs explain the redirect path behavior and restoration. (quarkus.io)Authorized redirect URIs. Add your external callback URL in Google Cloud. Mismatch is the most common 400/redirect error. (Google Help)
Secrets. Keep
client_secretin a secret store or environment variable. Never commit it.Session length and refresh tokens. Quarkus manages token refresh in web-app sessions if Google issued a refresh token. You can influence that with
access_type=offlineandprompt=consentas shown. (quarkus.io)Token propagation. We used the token propagation filter so the user’s access token flows to Google automatically from REST Client. If a provider gives opaque access tokens, you can inject
AccessTokenCredentialand pass it manually. (quarkus.io)Multi-tenant variants. You can map multiple providers or realms and select them dynamically with
TenantConfigResolver. We used it to request extra scopes on demand. (quarkus.io)
What-ifs and variations
Role mapping. Map RBAC roles from ID token or UserInfo if your org policies need it. Quarkus lets you read custom claims and apply HTTP auth policies.
Persistence. Replace H2 with PostgreSQL. Panache makes it a one-liner change in
application.properties.Drive/People integration. Request additional scopes only when needed. Keep
include_granted_scopes=trueto avoid re-asking for basic ones. Google API references cover available fields and scope combinations.
Troubleshooting
Looping back to login or 401 after consent. Check your
redirect_uriis exact and that you’ve whitelisted it in Google. Also verifyrestore-path-after-redirect=trueso you land back at your original path.userinfo missing fields. Ensure
user-info-required=trueand includeemail profilescopes. Some fields require People API calls.No Calendar data after connecting. Confirm the session actually has the calendar scope.
Security and compliance checklist
Use HTTPS in front of the app. Configure proxies so Quarkus builds correct absolute
redirect_uri.Keep
client_secretin a secret manager. Rotate it.Use strict CORS if you expose REST endpoints to SPAs.
Log only what you must. Never log tokens.
Limit scopes. Request Calendar scope only when needed (progressive authorization).
Ship features that matter, ask for permissions when they’re needed, and let identity do the heavy lifting.




