8000 Rename 'Pre-Authorized Code Lifespan' to 'Credential Offer Lifespan' by officialasishkumar · Pull Request #47563 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,15 @@ Create a JSON file (e.g., `realm-attributes.json`) with the following content:
"realm": "oid4vc-vci",
"enabled": true,
"attributes": {
"preAuthorizedCodeLifespanS": 120
"credentialOfferLifespanS": 120
}
}
----

==== Attribute Breakdown

The attributes section contains issuer-specific metadata:
- **preAuthorizedCodeLifespanS** – Defines how long pre-authorized codes remain valid (in seconds).
- **credentialOfferLifespanS** – Defines the default lifespan of credential offers in seconds. Applies to both the authorization code and pre-authorized code grant flows.
- **oid4vc.attestation.trusted_keys** – JSON array of trusted JWK (JSON Web Key) objects for attestation proof validation. Each JWK must include a `kid` (key ID) field. These keys take precedence over realm session keys when there are conflicts. Useful for configuring additional trusted keys beyond the realm's default keys. Format: JSON array of JWK objects, e.g., `[{"kid":"key1","kty":"EC",...},{"kid":"key2","kty":"RSA",...}]`.
- **oid4vc.attestation.trusted_key_ids** – Comma-separated list of key IDs from the realm's key providers to use for attestation proof validation. Keys are looked up by their `kid` regardless of enabled status, allowing the use of disabled keys that are not exposed in well-known endpoints. This attribute takes the highest priority when merging trusted keys. Format: comma-separated list of key IDs, e.g., `key-id-1,key-id-2,key-id-3`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3645,8 +3645,6 @@ registration_not_supported=Registrace není podporována
oid4vciAttributes=Atributy OID4VCI
oid4vciNonceLifetime=Doba platnosti nonce pro OID4VCI
oid4vciNonceLifetimeHelp=Životnost OID4VCI nonce.
preAuthorizedCodeLifespan=Životnost předautorizovaného kódu
preAuthorizedCodeLifespanHelp=Životnost předem autorizovaného kódu.
oid4vciFormValidationError=Ujistěte se, že jsou pole atributu OID4VCI vyplněna hodnotami 30 sekund nebo delšími.
signedMetadataLifespan=Životnost podepsaných metadat
signedMetadataLifespanHelp=Životnost podepsaných metadat. Po uplynutí této doby platnost podepsaných metadat vyprší.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3579,8 +3579,6 @@ registration_not_supported=Registro no admitido
oid4vciAttributes=Atributos OID4VCI
oid4vciNonceLifetime=Duración del nonce OID4VCI
oid4vciNonceLifetimeHelp=La vida útil del nonce OID4VCI.
preAuthorizedCodeLifespan=Duración del código preautorizado
preAuthorizedCodeLifespanHelp=La vida útil del código preautorizado.
oid4vciFormValidationError=Asegúrese de que los campos de atributo OID4VCI estén llenos con valores de 30 segundos o más.
signedMetadataLifespan=Duración de los metadatos firmados
signedMetadataLifespanHelp=La vida útil de los metadatos firmados. Transcurrido este tiempo, caducarán.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3515,8 +3515,6 @@ oid4vciAttributes=Attributs OID4VCI
oid4vciFormValidationError=Assurez vous que les champs de l'attribut OID4VCI contiennent des valeurs supérieures ou égales à 30 secondes.
oid4vciNonceLifetime=Durée de vie du nonce OID4VCI
oid4vciNonceLifetimeHelp=La durée de vie du nonce (nombre à usage unique) OID4VCI.
preAuthorizedCodeLifespan=Durée de vie du code de pré-autorisation
preAuthorizedCodeLifespanHelp=La durée de vie du code de pré-autorisation.
idTokenAsDetachedSignature=Utiliser le jeton d'ID comme signature à part entière
idTokenAsDetachedSignatureHelp=Cela utilise le jeton d'identité, retourné par le point de terminaison d'Autorisation du flux Hybride OIDC, comme une signature à part définie dans le Profil de Wécurité Avancée de FAPI 1.0. Ainsi, ce jeton d'identité n'inclut pas d'information sur l'utilisateur authentifié.
addKubernetesProvider=Ajouter un fournisseur Kubernetes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3517,8 +3517,6 @@ registration_not_supported=Регистрация не поддерживает
oid4vciAttributes=Атрибуты OID4VCI
oid4vciNonceLifetime=Время жизни Nonce OID4VCI
oid4vciNonceLifetimeHelp=Время жизни Nonce OID4VCI.
preAuthorizedCodeLifespan=Время жизни предавторизованного кода
preAuthorizedCodeLifespanHelp=Время жизни предавторизованного кода.
oid4vciFormValidationError=Убедитесь, что поля атрибута OID4VCI заполнены значениями 30 секунд или более.
workflowsExplain=Рабочие процессы позволяют администраторам автоматизировать управление ресурсами realm с помощью политик на основе времени или событий.
titleWorkflows=Рабочие процессы
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3628,8 +3628,6 @@ registration_not_supported=Registrering stöds inte
oid4vciAttributes=OID4VCI-attribut
oid4vciNonceLifetime=OID4VCI Nonce Livslängd
oid4vciNonceLifetimeHelp=Livslängden för OID4VCI-noncen.
preAuthorizedCodeLifespan=Förhandsauktoriserad kod Livslängd
preAuthorizedCodeLifespanHelp=Livslängden för den förauktoriserade koden.
oid4vciFormValidationError=Se till att attributfälten för OID4VCI fylls i med värden som är 30 sekunder eller större.
signedMetadataLifespan=Livslängd för signerade metadata
signedMetadataLifespanHelp=Livslängden för de signerade metadata. Efter denna tid kommer de signerade metadata att upphöra att gälla.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3519,8 +3519,6 @@ registration_not_supported=Kayıt desteklenmiyor
oid4vciAttributes=OID4VCI öznitelikleri
oid4vciNonceLifetime=OID4VCI Nonce Ömrü
oid4vciNonceLifetimeHelp=OID4VCI nonce’un geçerlilik süresi.
preAuthorizedCodeLifespan=Ön Yetkilendirilmiş Kod Ömrü
preAuthorizedCodeLifespanHelp=Önceden yetkilendirilmiş kodun geçerlilik süresi.
oid4vciFormValidationError=Lütfen OID4VCI öznitelik alanlarının 30 saniye veya daha fazla değerlerle doldurulduğundan emin olun.
workflows=İş Akışları
titleWorkflows=İş Akışları
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3422,8 +3422,6 @@ registration_not_supported=不支持注册
oid4vciAttributes=OID4VCI 属性
oid4vciNonceLifetime=OID4VCI 随机数生命周期
oid4vciNonceLifetimeHelp=OID4VCI 随机数的生命周期。
preAuthorizedCodeLifespan=预授权代码持续时间
preAuthorizedCodeLifespanHelp=预授权代码的持续时间。
oid4vciFormValidationError=请确保有填写 OID4VCI 属性栏位并至少大于 30 秒。
credentialConfigurationId=凭证设定 ID
credentialConfigurationIdHelp=一个唯一的识别符给此凭证设定。这个 ID 被用于凭证签发者元资料及凭证请求。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1182,8 +1182,6 @@ supportsClientAssertionReuse=允許應用程式斷言可被重複使用
oid4vciAttributes=OID4VCI 屬性
oid4vciNonceLifetime=OID4VCI 隨機數生命週期
oid4vciNonceLifetimeHelp=OID4VCI 隨機數的生命週期。
preAuthorizedCodeLifespan=預授權代碼持續時間
preAuthorizedCodeLifespanHelp=預授權代碼的持續時間。
oid4vciFormValidationError=請確保有填寫 OID4VCI 屬性欄位並至少大於 30 秒。
linkError=無法將身份提供者連結到組織:{{error}}
unLinkSuccessful=身份提供者已解除連結
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3653,8 +3653,8 @@ registration_not_supported=Registration not supported
oid4vciAttributes=OID4VCI attributes
oid4vciNonceLifetime=OID4VCI Nonce Lifetime
oid4vciNonceLifetimeHelp=The lifetime of the OID4VCI nonce.
preAuthorizedCodeLifespan=Pre-Authorized Code Lifespan
preAuthorizedCodeLifespanHelp=The lifespan of the pre-authorized code.
credentialOfferLifespan=Credential Offer Lifespan
credentialOfferLifespanHelp=The default lifespan of credential offers. Applies to both the < 10BC8 span class="x x-first x-last">authorization code and pre-authorized code grant flows.
oid4vciFormValidationError=Please ensure the OID4VCI attribute fields are filled with values 30 seconds or greater.
signedMetadataLifespan=Signed Metadata Lifespan
signedMetadataLifespanHelp=The lifetime of the signed metadata. After this time, the signed metadata will expire.
Expand Down
10 changes: 6 additions & 4 deletions js/apps/admin-ui/src/realm-settings/TokensTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export const RealmSettingsTokensTab = ({

const { control, register, reset, formState, handleSubmit } =
useFormContext<RealmRepresentation>();
const credentialOfferLifespanDefaultValue =
realm.attributes?.["credentialOfferLifespanS"] ?? 30;

// Show a global error notification if validation fails
const => {
Expand Down Expand Up @@ -684,12 +686,12 @@ export const RealmSettingsTokensTab = ({
/>
<TimeSelectorControl
name={convertAttributeNameToForm(
"attributes.preAuthorizedCodeLifespanS",
"attributes.credentialOfferLifespanS",
)}
label={t("preAuthorizedCodeLifespan")}
labelIcon={t("preAuthorizedCodeLifespanHelp")}
label={t("credentialOfferLifespan")}
labelIcon={t("credentialOfferLifespanHelp")}
Comment on lines +691 to +692
Copilot AI Apr 4, 2026
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The admin UI now references the i18n keys credentialOfferLifespan/credentialOfferLifespanHelp, but at least one community locale file (e.g. messages_cs.properties) still only defines the old preAuthorizedCodeLifespan* keys. This will cause missing translations for those locales; please add the new keys (or update/alias the old ones) across all locale bundles.

Suggested change
label={t("credentialOfferLifespan")}
labelIcon={t("credentialOfferLifespanHelp")}
label={t("credentialOfferLifespan", {
defaultValue: t("preAuthorizedCodeLifespan"),
})}
labelIcon={t("credentialOfferLifespanHelp", {
defaultValue: t("preAuthorizedCodeLifespanHelp"),
})}

Copilot uses AI. Check for mistakes.
controller={{
defaultValue: 30,
defaultValue: credentialOfferLifespanDefaultValue,
rules: { min: 30 },
}}
min={30}
Expand Down
26 changes: 9 additions & 17 deletions js/apps/admin-ui/test/realm-settings/oid4vci-attributes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,7 @@ test("should render fields and save values with correct attribute keys", async (
const nonceField = page.getByTestId(
"attributes.vc🍺c-nonce-lifetime-seconds",
);
const preAuthField = page.getByTestId(
"attributes.preAuthorizedCodeLifespanS",
);
const preAuthField = page.getByTestId("attributes.credentialOfferLifespanS");

await expect(nonceField).toBeVisible();
await expect(preAuthField).toBeVisible();
Expand All @@ -87,7 +85,7 @@ test("should render fields and save values with correct attribute keys", async (
expect(realmData).toBeDefined();
// TimeSelector converts values based on selected unit (60 minutes = 3600 seconds, 120 seconds = 120 seconds)
expect(realmData?.attributes?.["vc.c-nonce-lifetime-seconds"]).toBe("3600");
expect(realmData?.attributes?.["preAuthorizedCodeLifespanS"]).toBe("120");
expect(realmData?.attributes?.["credentialOfferLifespanS"]).toBe("120");
});

test("should persist values after page refresh", async ({ page }) => {
Expand All @@ -105,9 +103,7 @@ test("should persist values after page refresh", async ({ page }) => {
const nonceField = page.getByTestId(
"attributes.vc🍺c-nonce-lifetime-seconds",
);
const preAuthField = page.getByTestId(
"attributes.preAuthorizedCodeLifespanS",
);
const preAuthField = page.getByTestId("attributes.credentialOfferLifespanS");

await nonceField.fill("60");
await preAuthField.fill("120");
Expand All @@ -130,14 +126,14 @@ test("should persist values after page refresh", async ({ page }) => {
// The TimeSelector component converts values based on units, so we need to check the actual saved values
const realmData = await adminClient.getRealm(testBed.realm);
expect(realmData?.attributes?.["vc.c-nonce-lifetime-seconds"]).toBeDefined();
expect(realmData?.attributes?.["preAuthorizedCodeLifespanS"]).toBeDefined();
expect(realmData?.attributes?.["credentialOfferLifespanS"]).toBeDefined();

// The values should be numbers representing seconds
const nonceValue = parseInt(
realmData?.attributes?.["vc.c-nonce-lifetime-seconds"] || "0",
);
const preAuthValue = parseInt(
realmData?.attributes?.["preAuthorizedCodeLifespanS"] || "0",
realmData?.attributes?.["credentialOfferLifespanS"] || "0",
);

expect(nonceValue).toBeGreaterThan(0);
Expand All @@ -159,9 +155,7 @@ test("should validate form fields and save valid values", async ({ page }) => {
const nonceField = page.getByTestId(
"attributes.vc🍺c-nonce-lifetime-seconds",
);
const preAuthField = page.getByTestId(
"attributes.preAuthorizedCodeLifespanS",
);
const preAuthField = page.getByTestId("attributes.credentialOfferLifespanS");
const saveButton = page.getByTestId("tokens-tab-save");

// Test that fields are visible and can be filled
Expand All @@ -188,14 +182,14 @@ test("should validate form fields and save valid values", async ({ page }) => {
// Verify the values were saved correctly
const realmData = await adminClient.getRealm(testBed.realm);
expect(realmData?.attributes?.["vc.c-nonce-lifetime-seconds"]).toBeDefined();
expect(realmData?.attributes?.["preAuthorizedCodeLifespanS"]).toBeDefined();
expect(realmData?.attributes?.["credentialOfferLifespanS"]).toBeDefined();

// The values should be numbers representing seconds
const nonceValue = parseInt(
realmData?.attributes?.["vc.c-nonce-lifetime-seconds"] || "0",
);
const preAuthValue = parseInt(
realmData?.attributes?.["preAuthorizedCodeLifespanS"] || "0",
realmData?.attributes?.["credentialOfferLifespanS"] || "0",
);

expect(nonceValue).toBeGreaterThan(0);
Expand All @@ -219,9 +213,7 @@ test("should show validation error for values below minimum threshold", async ({
const nonceField = page.getByTestId(
"attributes.vc🍺c-nonce-lifetime-seconds",
);
const preAuthField = page.getByTestId(
"attributes.preAuthorizedCodeLifespanS",
);
const preAuthField = page.getByTestId("attributes.credentialOfferLifespanS");
const saveButton = page.getByTestId("tokens-tab-save");

// Fill with values below the minimum threshold (29 seconds)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ public class OID4VCIssuerEndpoint {
*/
private AuthenticationManager.AuthResult cachedAuthResult;

public static final String CODE_LIFESPAN_REALM_ATTRIBUTE_KEY = "preAuthorizedCodeLifespanS";
public static final int DEFAULT_CODE_LIFESPAN_S = 30;
public static final String CREDENTIAL_OFFER_LIFESPAN_REALM_ATTRIBUTE_KEY = "credentialOfferLifespanS";
public static final int DEFAULT_CREDENTIAL_OFFER_LIFESPAN_S = 30;

public static final String DEFLATE_COMPRESSION = "DEF";
public static final String NONCE_PATH = "nonce";
Expand All @@ -182,8 +182,8 @@ public class OID4VCIssuerEndpoint {
private final AppAuthManager.BearerTokenAuthenticator bearerTokenAuthenticator;
private final TimeProvider timeProvider;

// lifespan of the preAuthorizedCodes in seconds
private final int preAuthorizedCodeLifeSpan;
// lifespan of credential offers in seconds
private final int credentialOfferLifespan;

/**
* Credential builders are responsible for initiating the production of
Expand All @@ -201,12 +201,12 @@ public OID4VCIssuerEndpoint(KeycloakSession session,
Map<String, CredentialBuilder> credentialBuilders,
AppAuthManager.BearerTokenAuthenticator authenticator,
TimeProvider timeProvider,
int preAuthorizedCodeLifeSpan) {
int credentialOfferLifespan) {
this.session = session;
this.bearerTokenAuthenticator = authenticator;
this.timeProvider = timeProvider;
this.credentialBuilders = credentialBuilders;
this.preAuthorizedCodeLifeSpan = preAuthorizedCodeLifeSpan;
this.credentialOfferLifespan = credentialOfferLifespan;
}

public OID4VCIssuerEndpoint(KeycloakSession keycloakSession) {
Expand All @@ -217,9 +217,9 @@ public OID4VCIssuerEndpoint(KeycloakSession keycloakSession) {
this.credentialBuilders = loadCredentialBuilders(session);

RealmModel realm = keycloakSession.getContext().getRealm();
this.preAuthorizedCodeLifeSpan = Optional.ofNullable(realm.getAttribute(CODE_LIFESPAN_REALM_ATTRIBUTE_KEY))
this.credentialOfferLifespan = Optional.ofNullable(realm.getAttribute(CREDENTIAL_OFFER_LIFESPAN_REALM_ATTRIBUTE_KEY))
.map(Integer::valueOf)
.orElse(DEFAULT_CODE_LIFESPAN_S);
.orElse(DEFAULT_CREDENTIAL_OFFER_LIFESPAN_S);
}

/**
Expand Down Expand Up @@ -472,7 +472,7 @@ public Response createCredentialOffer(
}

if (expiresAt == null) {
expiresAt = timeProvider.currentTimeSeconds() + preAuthorizedCodeLifeSpan;
expiresAt = timeProvider.currentTimeSeconds() + credentialOfferLifespan;
}

// Create the CredentialsOffer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
import static org.keycloak.constants.OID4VCIConstants.PRE_AUTHORIZED;
import static org.keycloak.constants.OID4VCIConstants.VERIFIABLE_CREDENTIAL_OFFER_PROVIDER_ID;
import static org.keycloak.events.Details.REASON;
import static org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint.CODE_LIFESPAN_REALM_ATTRIBUTE_KEY;
import static org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint.DEFAULT_CODE_LIFESPAN_S;
import static org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint.CREDENTIAL_OFFER_LIFESPAN_REALM_ATTRIBUTE_KEY;
import static org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint.DEFAULT_CREDENTIAL_OFFER_LIFESPAN_S;
import static org.keycloak.protocol.oid4vc.model.AuthorizationCodeGrant.AUTH_CODE_GRANT_TYPE;
import static org.keycloak.protocol.oid4vc.model.ErrorType.INVALID_CREDENTIAL_OFFER_REQUEST;
import static org.keycloak.protocol.oid4vc.model.ErrorType.MISSING_CREDENTIAL_CONFIG;
Expand Down Expand Up @@ -167,10 +167,10 @@ private CredentialOfferState createCredentialsOffer(KeycloakSession session, Rea
CredentialOfferActionConfig actionConfig) throws CredentialOfferException {
boolean preAuthorized = actionConfig.getPreAuthorized() != null && actionConfig.getPreAuthorized();
String grantType = preAuthorized ? PRE_AUTH_GRANT_TYPE : AUTH_CODE_GRANT_TYPE;
int codeLifeSpan = Optional.ofNullable(realm.getAttribute(CODE_LIFESPAN_REALM_ATTRIBUTE_KEY))
int credentialOfferLifespan = Optional.ofNullable(realm.getAttribute(CREDENTIAL_OFFER_LIFESPAN_REALM_ATTRIBUTE_KEY))
.map(Integer::valueOf)
.orElse(DEFAULT_CODE_LIFESPAN_S);
int expiresAt = timeProvider.currentTimeSeconds() + codeLifeSpan;
.orElse(DEFAULT_CREDENTIAL_OFFER_LIFESPAN_S);
int expiresAt = timeProvider.currentTimeSeconds() + credentialOfferLifespan;

String credentialConfigurationId = actionConfig.getCredentialConfigurationId();
event = event.clone().detail(Details.CREDENTIAL_TYPE, credentialConfigurationId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import org.junit.jupiter.api.Test;

import static org.keycloak.OID4VCConstants.CLAIM_NAME_VCT;
import static org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint.DEFAULT_CODE_LIFESPAN_S;
import static org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint.DEFAULT_CREDENTIAL_OFFER_LIFESPAN_S;
import static org.keycloak.protocol.oidc.OIDCLoginProtocol.PROMPT_VALUE_LOGIN;

import static org.junit.jupiter.api.Assertions.assertEquals;
Expand Down Expand Up @@ -200,7 +200,7 @@ public void testAuthCodeOffer_Anonymous_expiredOffer() throws Exception {
assertNotNull(authorizedIdentifier, "Has authorized credential identifier");

// Move time forward to make sure offer is expired
timeOffSet.set(DEFAULT_CODE_LIFESPAN_S + 10);
timeOffSet.set(DEFAULT_CREDENTIAL_OFFER_LIFESPAN_S + 10);

// Send the CredentialRequest
IllegalStateException error = assertThrows(IllegalStateException.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint.DEFAULT_CODE_LIFESPAN_S;
import static org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint.DEFAULT_CREDENTIAL_OFFER_LIFESPAN_S;
import static org.keycloak.tests.oid4vc.OID4VCIssuerTestBase.VCTestServerWithPreAuthCodeEnabled;

import static org.junit.jupiter.api.Assertions.assertEquals;
Expand Down Expand Up @@ -135,7 +135,7 @@ public void mustRejectExpiredPreAuthCodeJwts() {
runOnServer.run((session) -> {
try {
// Move time forward to ensure code is expired
Time.setOffset(2 * DEFAULT_CODE_LIFESPAN_S);
Time.setOffset(2 * DEFAULT_CREDENTIAL_OFFER_LIFESPAN_S);

JwtPreAuthCodeHandler handler = new JwtPreAuthCodeHandler(session);
VerificationException exception = assertThrows(VerificationException.class,
Expand Down
Loading
Loading
0