Skip to content
Draft
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
84b2e7e
Make Keycloak an OpenID4VP verifier for user authentication (#105)
IngridPuppet Aug 21, 2025
2839131
[Datev Stable] Add support for parsing and understanding authorizatio…
Awambeng Aug 26, 2025
6de6040
[Datev Stable] Understand key attestations as additional information …
Awambeng Aug 26, 2025
4c2a28c
Centralize OID4VCI Protocol Constants in Oid4VciConstants and Refacto…
Awambeng Aug 26, 2025
4c104ac
[Datev Stable] Create dedicated constants-class (#113)
Awambeng Aug 26, 2025
71e2690
[Datev Stable] Extend realm UI configuration by OID4VCI attributes (#…
Awambeng Aug 26, 2025
c0f96e5
Update OID4VCI error handling for draft 16 specification (#118)
Awambeng Aug 27, 2025
ccdca75
Add invalid_nonce error support for OID4VCI (#121)
Awambeng Aug 27, 2025
b01e73e
[Datev Stable] Update batch_credential_issuance to enforce batch_size…
Awambeng Aug 27, 2025
87d4906
[Datev Stable] Restructure credential_configurations_supported parsin…
Awambeng Aug 27, 2025
d144c94
[Datev Stable] Rename ldp_vp to di_vp and restructure proofs object (…
Awambeng Aug 28, 2025
5d380fa
Update authorization_details for OID4VCI draft-16 compliance
forkimenjeckayang Sep 1, 2025
ea95df8
[Datev Stable] Implement support for credential_request_encryption (#…
Awambeng Sep 1, 2025
d512a6b
[Datev Stable] Implement ReferencedTokenValidator for Token Status Li…
forkimenjeckayang Sep 2, 2025
a59c2a2
update: address comments by @IngridPuppet
forkimenjeckayang Sep 3, 2025
ba36142
fix: resolved merge conflicts
forkimenjeckayang Sep 3, 2025
c2f7f13
update: address review comments by @IngridPuppet
forkimenjeckayang Sep 4, 2025
b2d10b5
update: Re-throw BadRequestException to ensure client receives proper…
forkimenjeckayang Sep 8, 2025
d0b11ad
update: wrap BadRequestException with helper method getErrorResponse
forkimenjeckayang Sep 8, 2025
f1e98c1
[Datev Stable] Implement multiple credential issuance (#132)
Awambeng Sep 8, 2025
3bb6513
fix: resolved merge conflicts
forkimenjeckayang Sep 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/src/main/java/org/keycloak/common/Profile.java
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ public enum Feature {

OID4VC_VCI("Support for the OID4VCI protocol as part of OID4VC.", Type.EXPERIMENTAL),

OID4VC_VPAUTH("Support for the OID4VP protocol for user authentication", Type.EXPERIMENTAL),

OPENTELEMETRY("OpenTelemetry Tracing", Type.DEFAULT),

DECLARATIVE_UI("declarative ui spi", Type.EXPERIMENTAL),
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/java/org/keycloak/OAuth2Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ public interface OAuth2Constants {
String AUTHENTICATOR_METHOD_REFERENCE = "amr";

String CNF = "cnf";

String AUTHORIZATION_DETAILS_PARAM = "authorization_details";
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.keycloak.sdjwt.consumer;

import java.io.IOException;

/**
* Functional interface for fetching Status List JWT tokens.
*
* @author <a href="mailto:[email protected]">Forkim Akwichek</a>
*/
public interface StatusListJwtFetcher {

/**
* Performs an HTTP GET at the URI and returns the response as a JWT string.
* This method is specifically for fetching Status List JWT tokens with the
* appropriate Accept header (application/statuslist+jwt).
*
* @param uri The URI to fetch the Status List JWT from
* @return The Status List JWT as a string
* @throws IOException if I/O error or HTTP status not OK (200)
*/
String fetchStatusListJwt(String uri) throws IOException;
}
1 change: 1 addition & 0 deletions docs/documentation/server_admin/topics.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ include::topics/assembly-managing-organizations.adoc[]
include::topics/assembly-managing-clients.adoc[]
ifeval::[{project_community}==true]
include::topics/oid4vci/vc-issuer-configuration.adoc[]
include::topics/oid4vp/oid4vp-user-authentication.adoc[]
endif::[]
include::topics/vault.adoc[]
include::topics/events.adoc[]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
[[oid4vp-authentication]]
== Authenticate users with verifiable credentials over OpenID4VP

{project_name} implements experimental support for authenticating users as they present verifiable credentials issued to them by itself.
This is done over https://openid.net/specs/openid-4-verifiable-presentations-1_0-20.html[OpenID for Verifiable Presentation (OpenID4VP)].
This chapter describes how to interact with this feature.

[IMPORTANT]
====
This is an experimental feature and should not be used in production.
Backward compatibility is not guaranteed, and future updates may introduce breaking changes.
====

=== What is covered in this chapter

- Enabling user authentication over OpenID4VP
- Initiating the flow by retrieving an authentication request
- Polling for the status of the authentication session

=== Enabling user authentication over OpenID4VP

To enable user authentication over OpenID4VP, make sure to add the following flags to the startup command:

[source,bash]
----
--features=oid4vc-vpauth,oid4vc-vci
----

Verify activation by checking the server logs.
The second `oid4vc-vci` feature must equally be activated because the feature relies on {project_name} being able to link:../oid4vci/vc-issuer-configuration.adoc[issue verifiable credentials] to users itself.

=== Initiating the flow by retrieving an authentication request

To retrieve an authentication request, send a POST request to the `/oid4vp-auth/request` endpoint.
The request must be sent form-urlencoded and embed credentials for client authentication.

[source,bash]
----
POST /realms/<realm>/oid4vp-auth/request
Content-Type: application/x-www-form-urlencoded
client_id=<client_id>&client_secret=<client_secret>
----

The endpoint returns an object containing:

* an OpenID4VP authorization request link that can be used to initiate the authentication flow, either directly or by generating a QR code from it;
* a transaction ID for inquiring the status of the thereby opened authentication session.

[source,json]
----
{
"authorization_request": "openid4vp://?client_id=<client_id>&request_uri=<request_uri>",
"transaction_id": "<transaction_id>"
}
----

=== Polling for the status of the authentication session

Because the OpenID4VP authentication flow is implied to be pursued cross-device, the initiating client needs to asynchronously check the status of the authentication session.
For this, send a GET request to the `/oid4vp-auth/status` endpoint, passing the transaction ID received in the previous step.

[source,bash]
----
GET /realms/<realm>/oid4vp-auth/status/<transaction_id>
----

The response is an object containing the status of the authentication session, which can be one of the following: `pending`, `success`, or `error`.
In case of a successful authentication, the response also contains an *authorization code* to exchange for an access token.
In case of an error, the response contains error details.

.Pending authentication
[source,json]
----
{
"status": "pending"
}
----

.Successful authentication
[source,json]
----
{
"status": "success",
"authorization_code": "<code>"
}
----

.Error during authentication
[source,json]
----
{
"status": "error",
"error": "<error_type>",
"error_description": "<error_description>"
}
----
Original file line number Diff line number Diff line change
Expand Up @@ -3528,3 +3528,10 @@ oid4vciEnabled=Enable OID4VCI
oid4vciEnabledHelp=Enable this option to allow the client to request verifiable credentials from Keycloak's OID4VCI credential endpoint.
noAccessPolicies=No access policies
noAccessPoliciesInstructions=There haven't been configured any access policies yet. Click the button below to configure the first policy.
oid4vciAttributes=OID4VCI attributes
oid4vciAttributesSectionTitle=General
oid4vciNonceLifetime=OID4VCI Nonce Lifetime
oid4vciNonceLifetimeHelp=The lifetime of the OID4VCI nonce in seconds.
preAuthorizedCodeLifespan=Pre-Authorized Code Lifespan
preAuthorizedCodeLifespanHelp=The lifespan of the pre-authorized code in seconds.
formValidationError=Please ensure all fields are filled and values are 30 seconds or greater.
132 changes: 132 additions & 0 deletions js/apps/admin-ui/src/realm-settings/RealmSettingsOid4vciTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {
PageSection,
ActionGroup,
Button,
FormGroup,
} from "@patternfly/react-core";
import { TimeSelector } from "../components/time-selector/TimeSelector";
import { convertToFormValues } from "../util";
import { useEffect } from "react";
import { Controller, useFormContext, FormProvider } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { FormAccess } from "../components/form/FormAccess";
import { HelpItem, FormPanel, useAlerts } from "@keycloak/keycloak-ui-shared";
import { AlertVariant } from "@patternfly/react-core";

export const RealmSettingsOid4vciTab = ({
realm,
save,
}: {
realm: any;
save: (realm: any) => void;
}) => {
const { t } = useTranslation();
const { addAlert } = useAlerts();
const form = useFormContext();
const { formState, handleSubmit, setValue } = form;

// Show a global error notification if validation fails
const onError = () => {
addAlert(t("formValidationError"), AlertVariant.danger);
};

// Hydrate form values from realm attributes
useEffect(() => {
if (realm.attributes) {
// Set the nonce lifetime value if it exists in attributes
if (realm.attributes["vc.c-nonce-lifetime-seconds"]) {
setValue(
"attributes.vc.c-nonce-lifetime-seconds",
realm.attributes["vc.c-nonce-lifetime-seconds"],
);
}
// Add any other vc attributes that need hydration here
}
}, [realm.attributes, setValue]);

return (
<PageSection variant="light">
<FormPanel title={t("oid4vciAttributesSectionTitle")}>
<FormProvider {...form}>
<FormAccess
isHorizontal
role="manage-realm"
className="pf-u-mt-lg"
onSubmit={handleSubmit(save, onError)}
>
<FormGroup
label={t("oid4vciNonceLifetime")}
fieldId="oid4vciNonceLifetime"
labelIcon={
<HelpItem
helpText={t("oid4vciNonceLifetimeHelp")}
fieldLabelId="oid4vciNonceLifetime"
/>
}
>
<Controller
name="attributes.vc.c-nonce-lifetime-seconds"
control={form.control}
rules={{ required: t("required"), min: 30 }}
render={({ field }) => (
<TimeSelector
{...field}
id="oid4vciNonceLifetime"
min={30}
units={["second", "minute", "hour"]}
value={field.value}
onChange={field.onChange}
data-testid="oid4vci-nonce-lifetime-seconds"
/>
)}
/>
</FormGroup>
<FormGroup
label={t("preAuthorizedCodeLifespan")}
fieldId="preAuthorizedCodeLifespan"
labelIcon={
<HelpItem
helpText={t("preAuthorizedCodeLifespanHelp")}
fieldLabelId="preAuthorizedCodeLifespan"
/>
}
>
<Controller
name="attributes.preAuthorizedCodeLifespanS"
control={form.control}
rules={{ required: t("required"), min: 30 }}
render={({ field }) => (
<TimeSelector
{...field}
id="preAuthorizedCodeLifespan"
min={30}
units={["second", "minute", "hour"]}
value={field.value}
onChange={field.onChange}
data-testid="pre-authorized-code-lifespan-s"
/>
)}
/>
</FormGroup>
<ActionGroup>
<Button
variant="primary"
type="submit"
data-testid="oid4vci-tab-save"
isDisabled={!formState.isDirty}
>
{t("save")}
</Button>
<Button
variant="link"
onClick={() => convertToFormValues(realm, setValue)}
>
{t("revert")}
</Button>
</ActionGroup>
</FormAccess>
</FormProvider>
</FormPanel>
</PageSection>
);
};
39 changes: 39 additions & 0 deletions js/apps/admin-ui/src/realm-settings/RealmSettingsTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { ClientPoliciesTab, toClientPolicies } from "./routes/ClientPolicies";
import { RealmSettingsTab, toRealmSettings } from "./routes/RealmSettings";
import { SecurityDefenses } from "./security-defences/SecurityDefenses";
import { UserProfileTab } from "./user-profile/UserProfileTab";
import { RealmSettingsOid4vciTab } from "./RealmSettingsOid4vciTab";

export interface UIRealmRepresentation extends RealmRepresentation {
upConfig?: UserProfileConfig;
Expand Down Expand Up @@ -235,6 +236,34 @@ export const RealmSettingsTabs = () => {
),
);
}
if (r.attributes) {
// Handle nested vc object created by React Hook Form
if (r.attributes["vc"] && typeof r.attributes["vc"] === "object") {
const vcObj = r.attributes["vc"] as Record<string, any>;
Object.keys(vcObj).forEach((key) => {
const flatKey = `vc.${key}`;
r.attributes![flatKey] = vcObj[key]?.toString() || vcObj[key];
});
// Remove the nested object
delete r.attributes["vc"];
}

// Convert OID4VCI attributes to strings if they're not already
if (
r.attributes["vc.c-nonce-lifetime-seconds"] !== undefined &&
typeof r.attributes["vc.c-nonce-lifetime-seconds"] !== "string"
) {
r.attributes["vc.c-nonce-lifetime-seconds"] =
r.attributes["vc.c-nonce-lifetime-seconds"].toString();
}
if (
r.attributes["preAuthorizedCodeLifespanS"] !== undefined &&
typeof r.attributes["preAuthorizedCodeLifespanS"] !== "string"
) {
r.attributes["preAuthorizedCodeLifespanS"] =
r.attributes["preAuthorizedCodeLifespanS"].toString();
}
}

try {
const savedRealm: UIRealmRepresentation = {
Expand Down Expand Up @@ -287,6 +316,7 @@ export const RealmSettingsTabs = () => {
const clientPoliciesTab = useTab("client-policies");
const userProfileTab = useTab("user-profile");
const userRegistrationTab = useTab("user-registration");
const oid4vciTab = useTab("oid4vci-attributes");
const { hasAccess, hasSomeAccess } = useAccess();
const canViewOrManageEvents =
hasAccess("view-realm") && hasSomeAccess("view-events", "manage-events");
Expand Down Expand Up @@ -463,6 +493,15 @@ export const RealmSettingsTabs = () => {
<UserRegistration />
</Tab>
)}
{isFeatureEnabled(Feature.OpenId4VCI) && (
<Tab
title={<TabTitleText>{t("oid4vciAttributes")}</TabTitleText>}
data-testid="rs-oid4vci-attributes-tab"
{...oid4vciTab}
>
<RealmSettingsOid4vciTab realm={realm!} save={save} />
</Tab>
)}
</RoutableTabs>
</PageSection>
</FormProvider>
Expand Down
3 changes: 2 additions & 1 deletion js/apps/admin-ui/src/realm-settings/routes/RealmSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export type RealmSettingsTab =
| "tokens"
| "client-policies"
| "user-profile"
| "user-registration";
| "user-registration"
| "oid4vci-attributes";

export type RealmSettingsParams = {
realm: string;
Expand Down
Loading