Getting Started
Run an authorization server and a resource API as two separate Quarkus applications. Sign in on the authorization server, let an OAuth client obtain an access token with Authorization Code, and use that token to call the API protected by quarkus-oidc.
The authorization server owns the users and authenticates them through Quarkus Form. The resource API trusts tokens from that server; it does not look up the user's password or use the server's login session. This works for a new system or one with an existing user service.
| Role | In this walkthrough | Responsibility |
|---|---|---|
| Authorization server | authorization-server, port 8080 | User login, consent, client authentication and token issuance |
| Resource server | resource-server, port 8081 | Validate the access token and require message.read for /api/messages |
| OAuth client | Browser and terminal commands | Start authorization, receive a code, exchange it for a token and call the API |
The browser redirects between the authorization server and the callback page. The terminal sends the code to the authorization server's token endpoint, then sends the access token to the resource API. The resource application hosts a static callback helper so this walkthrough needs no frontend build or third server; it does not perform the token exchange.
You need JDK 21, Maven 3.9+, curl 7.76+, Python 3, and a browser. Keep ports 8080 and 8081 free and use localhost consistently. You do not need this repository's source or a database.
1. Create the authorization server
Create a working directory and the first application:
mkdir oauth-quickstart
cd oauth-quickstart
mvn io.quarkus.platform:quarkus-maven-plugin:3.33.3.1:create \
-DplatformVersion=3.33.3.1 \
-DprojectGroupId=org.acme \
-DprojectArtifactId=authorization-server \
-Dextensions=config-yaml,elytron-security-common \
-DnoCode
cd authorization-serverAdd the authorization-server dependency to the top-level <dependencies> section in this application's pom.xml:
<dependency>
<groupId>io.github.flynndi</groupId>
<artifactId>quarkus-authorization-server</artifactId>
<version>1.0.0-beta2</version>
</dependency>For an existing application, use the Quarkus 3.33.3.1 platform BOM and add these dependencies:
| Dependency | Purpose |
|---|---|
io.github.flynndi:quarkus-authorization-server:1.0.0-beta2 | Authorization endpoints, client registration, token issuance and default browser pages |
io.quarkus:quarkus-config-yaml | Read application.yml |
io.quarkus:quarkus-elytron-security-common | Verify this demo user's password with BcryptUtil |
The platform manages the io.quarkus dependency versions. Applications consume the authorization-server runtime artifact; Quarkus resolves its deployment artifact automatically. This application does not need quarkus-oidc or Quarkus REST to serve the authorization endpoints.
2. Provide user authentication through CDI
In authorization-server, create src/main/java/org/acme/DemoIdentityProviders.java:
package org.acme;
import java.util.Map;
import java.util.Set;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Singleton;
import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.IdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.TrustedAuthenticationRequest;
import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest;
import io.quarkus.security.runtime.QuarkusPrincipal;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;
@Singleton
public class DemoIdentityProviders {
// This application owns its users. Replace the map with your user service.
private final Map<String, User> users = Map.of(
"alice", new User("alice", BcryptUtil.bcryptHash("alice-password"),
Set.of("user"), Set.of("message.read")));
@Produces
@Singleton
public IdentityProvider<UsernamePasswordAuthenticationRequest> passwordIdentityProvider() {
return new IdentityProvider<>() {
@Override
public Class<UsernamePasswordAuthenticationRequest> getRequestType() {
return UsernamePasswordAuthenticationRequest.class;
}
@Override
public Uni<SecurityIdentity> authenticate(UsernamePasswordAuthenticationRequest request,
AuthenticationRequestContext context) {
return context.runBlocking(() -> {
User user = findUser(request.getUsername());
if (!BcryptUtil.matches(new String(request.getPassword().getPassword()), user.passwordHash())) {
throw new AuthenticationFailedException();
}
return identity(user);
});
}
};
}
@Produces
@Singleton
public IdentityProvider<TrustedAuthenticationRequest> trustedIdentityProvider() {
return new IdentityProvider<>() {
@Override
public Class<TrustedAuthenticationRequest> getRequestType() {
return TrustedAuthenticationRequest.class;
}
@Override
public Uni<SecurityIdentity> authenticate(TrustedAuthenticationRequest request,
AuthenticationRequestContext context) {
// Quarkus Form has validated the cookie; reload the application's user.
return context.runBlocking(() -> identity(findUser(request.getPrincipal())));
}
};
}
private User findUser(String username) {
User user = this.users.get(username);
if (user == null) {
throw new AuthenticationFailedException();
}
return user;
}
private static SecurityIdentity identity(User user) {
return QuarkusSecurityIdentity.builder()
.setPrincipal(new QuarkusPrincipal(user.username()))
.addRoles(user.roles())
.addPermissionsAsString(user.permissions())
.build();
}
private record User(String username, String passwordHash, Set<String> roles, Set<String> permissions) {
}
}This demo has one user, alice / alice-password, with a username, password hash, roles and permissions. Replace the map with your user service when integrating your application.
Both providers share that user service:
UsernamePasswordAuthenticationRequest: verify the password submitted to Quarkus Form.TrustedAuthenticationRequest: reload the user's identity on subsequent requests, after Quarkus Form has validated its encrypted login cookie.
These beans belong only to the authorization server. The resource server uses quarkus-oidc to establish an identity from the access token.
3. Configure the client and login
In authorization-server, create src/main/resources/application.yml. If the generated application has an empty application.properties, remove it.
quarkus:
http:
port: 8080
auth:
form:
enabled: true
authorization-server:
issuer: http://localhost:8080
default-login-page-enabled: true
# Publish OIDC discovery and JWKS for the resource server.
oidc:
enabled: true
clients:
quickstart-client:
client-name: Quickstart
client-authentication-methods: client_secret_basic
# Bcrypt hash of the demo client secret: quickstart-secret.
client-secret: '$2a$10$ADV8VEBZhxueuLTSw6i9LO7efLWg25a1PW1RH93GlPbFng/E3v5tO'
authorization-grant-types: authorization_code
redirect-uris: http://localhost:8081/callback.html
scopes: message.read
require-proof-key: false
require-authorization-consent: truequarkus.authorization-server serves the OAuth endpoints, registers quickstart-client in the default in-memory repository, and enables the built-in login page. quarkus.http.auth.form.enabled enables Quarkus Form authentication. Enabling OIDC publishes discovery metadata so the separate resource server can discover the issuer and its JWKS.
This is a confidential client: it authenticates at the token endpoint with HTTP Basic, using client ID quickstart-client and client secret quickstart-secret. The YAML already contains the bcrypt hash of that demo secret; copy it as shown. require-proof-key: false keeps PKCE out of this walkthrough. Consent for message.read is still required.
The map key quickstart-client is the client ID. The registered callback on port 8081 must exactly match the URI in the authorization and token requests. The terminal stands in for a backend OAuth client; the client secret is used only for token exchange. Do not put it in the callback page or frontend JavaScript.
For this local walkthrough, the defaults provide in-memory client, authorization and consent repositories and a temporary RSA signing key. No repository bean or PEM file is needed. Restarting the authorization server loses authorizations and consent and changes the signing key.
4. Create the resource server
Return to the oauth-quickstart parent directory and create a second application alongside the first:
cd ..
mvn io.quarkus.platform:quarkus-maven-plugin:3.33.3.1:create \
-DplatformVersion=3.33.3.1 \
-DprojectGroupId=org.acme \
-DprojectArtifactId=resource-server \
-Dextensions=rest-jackson,config-yaml,oidc \
-DnoCode
cd resource-serverThis application uses quarkus-rest-jackson for its JSON API, quarkus-config-yaml for configuration and quarkus-oidc for Bearer token validation. It does not need the authorization-server dependency, the user authentication beans or a signing private key.
5. Configure the resource API and callback page
In resource-server, create src/main/resources/application.yml. Remove its empty application.properties if present.
quarkus:
http:
port: 8081
auth:
permission:
resources:
paths: /api/*
policy: authenticated
auth-mechanism: Bearer
oidc:
application-type: service
# Discover the authorization server in the other process.
auth-server-url: http://localhost:8080
token:
audience: quickstart-client
allow-jwt-introspection: false
allow-opaque-token-introspection: falseThe API listens on 8081; quarkus.oidc.auth-server-url points to the authorization server on 8080. quarkus-oidc discovers its public keys and validates the token's signature, issuer, expiry and audience. The audience must contain quickstart-client, the authorization server's default audience for this client.
The /api/* rule selects Bearer authentication. A Form login cookie alone cannot access the API. This resource server uses local JWT validation and does not send the user's password to the authorization server or call the token endpoint to authenticate each API request.
Create src/main/java/org/acme/MessageResource.java in resource-server:
package org.acme;
import java.util.Map;
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;
import io.quarkus.security.PermissionsAllowed;
import io.quarkus.security.identity.SecurityIdentity;
@Path("/api/messages")
@Produces(MediaType.APPLICATION_JSON)
public class MessageResource {
@Inject
SecurityIdentity identity;
@GET
@PermissionsAllowed("message.read")
public Map<String, String> messages() {
return Map.of("subject", this.identity.getPrincipal().getName(), "message", "Hello, OAuth!");
}
}quarkus-oidc maps the validated token's scopes to permissions. @PermissionsAllowed("message.read") checks the permission required by this endpoint.
Also in resource-server, create src/main/resources/META-INF/resources/callback.html:
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="referrer" content="no-referrer">
<title>OAuth callback</title>
<h1>Return to your terminal</h1>
<p>Copy the complete URL from the address bar, including code and state.</p>
<p>The quickstart command checks state and exchanges the code for an access token.</p>
</html>This publicly accessible static page is only the tutorial client's callback destination. The terminal checks state and exchanges the code. In your own application, register the actual OAuth client's callback; serving a callback page is not a resource-server requirement.
6. Start both applications
Open two terminals, each initially in oauth-quickstart. In the first, start the authorization server:
cd authorization-server
mvn quarkus:devWait for http://localhost:8080 to start. In the second terminal, check discovery, then start the resource server:
curl --fail-with-body -sS http://localhost:8080/.well-known/openid-configuration
cd resource-server
mvn quarkus:dev -Ddebug=5006Wait for http://localhost:8081 to start. The second debug port avoids a clash with the authorization server's dev-mode debugger. Start the authorization server first so the resource server can reach discovery during startup. If you restart the authorization server with a new temporary key, restart the resource server and obtain a new token for this walkthrough.
7. Sign in and obtain an access token
In a third terminal, generate a random state and the authorization URL. state binds the callback to the request you started. Keep this terminal open for the remaining commands:
export QAS_STATE=$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')
printf '%s\n' "http://localhost:8080/oauth2/authorize?response_type=code&client_id=quickstart-client&redirect_uri=http%3A%2F%2Flocalhost%3A8081%2Fcallback.html&scope=message.read&state=$QAS_STATE"Open the printed URL in your browser:
- Sign in on
localhost:8080asalicewith passwordalice-password. - Select the
message.readcheckbox, then click Approve. - The browser redirects to
http://localhost:8081/callback.html?code=…&state=…. - Copy the complete callback URL from the address bar.
Login authenticates the user; the callback contains an authorization code, not an access token. Exchange the code at the authorization server on 8080, using the client ID and secret.
In the same terminal, first run the following command, paste the callback URL when read waits for input, and press Enter:
read -r QAS_CALLBACKThen validate state and exchange the code using HTTP Basic client authentication:
export QAS_CALLBACK
unset QAS_ACCESS_TOKEN QAS_TOKEN_RESPONSE
QAS_CODE=$(python3 - <<'PY'
import os, sys
from urllib.parse import parse_qs, urlsplit
callback = urlsplit(os.environ["QAS_CALLBACK"])
params = parse_qs(callback.query)
if (callback.scheme, callback.netloc, callback.path) != ("http", "localhost:8081", "/callback.html"):
sys.exit("Unexpected callback URL")
if params.get("state") != [os.environ["QAS_STATE"]]:
sys.exit("State mismatch: restart authorization")
if "error" in params:
sys.exit("Authorization failed: " + params["error"][0])
codes = params.get("code", [])
if len(codes) != 1:
sys.exit("Expected one authorization code")
print(codes[0])
PY
) && QAS_TOKEN_RESPONSE=$(curl --fail-with-body -sS \
-u quickstart-client:quickstart-secret \
--data-urlencode grant_type=authorization_code \
--data-urlencode redirect_uri=http://localhost:8081/callback.html \
--data-urlencode "code=$QAS_CODE" \
http://localhost:8080/oauth2/token) && \
QAS_ACCESS_TOKEN=$(printf '%s' "$QAS_TOKEN_RESPONSE" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')On success, QAS_ACCESS_TOKEN contains the access token. curl -u sends the client credentials in the HTTP Basic header. These credentials authenticate the OAuth client; alice / alice-password authenticates the user on the login page. The token response includes token_type: Bearer and expires_in; this client does not request a refresh token or ID Token.
An authorization code is short-lived and can only be used once. If it expires, is already used, or the state check fails, repeat this section to start a new authorization request. If you deny consent, the callback contains an OAuth error and the command stops before requesting a token.
8. Call the resource server
In the same terminal, send the access token to the resource API on 8081:
curl --fail-with-body -sS -H "Authorization: Bearer $QAS_ACCESS_TOKEN" \
http://localhost:8081/api/messagesExpected JSON, regardless of field order:
{"subject":"alice","message":"Hello, OAuth!"}The resource API returns 401 without a token. You can check this separately:
curl -i http://localhost:8081/api/messagesAn authenticated token without the required message.read permission receives 403. The resource API makes this access decision independently of the authorization server's login session.
The user's roles and permissions are not automatically copied into the token or intersected with client scopes. This example authorizes API access through the granted scope; add your own user-access policy where your application needs it. See Identity and access.
Continue with your application
- Configuration reference: client settings, default pages, endpoint paths and CDI replacements.
- Storage and keys: durable repositories, signing keys and the separate Quarkus Form session encryption key.
- Tokens and resource servers: issuer and audience validation, token representations and custom claims.
- Authorization Code + PKCE: continue with a public Vue client using PKCE, OIDC and logout. The repository examples are optional runnable references.
- Client Credentials: machine-to-machine access without user login.
Before deployment, use HTTPS and your public issuer and client callback URLs, replace demo credentials, and configure persistent keys and storage. The authorization server and resource API can run on different hosts; set the resource server's quarkus.oidc.auth-server-url to the authorization server's public issuer.