mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-16 20:15:46 -06:00
New JWT Authorization Grant Identity provider (#44176)
Closes #43570 Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
This commit is contained in:
committed by
GitHub
parent
731414e44a
commit
3e8b2f8ab7
@@ -896,6 +896,7 @@ deleteError=Could not delete the provider {{error}}
|
||||
attributeDisplayName=Display name
|
||||
pkceEnabled=Use PKCE
|
||||
authorizationGrantSettings=Authorization Grant Settings
|
||||
authorizationGrantSettingsHelp= This section is used to configure specific settings related to JWT Authorization Grant support as defined in RFC 7523. Not all settings are configured here, some belong to the identity provider, such as JWKS URL, Issuer and Allowed Clock Skew.
|
||||
jwtAuthorizationGrantIdpEnabled=JWT Authorization Grant
|
||||
jwtAuthorizationGrantIdpEnabledHelp=Enable the identity provider to act as a trust provider to validate authorization grant JWT assertions according to RFC 7523.
|
||||
jwtAuthorizationGrantAssertionReuseAllowed=Allow assertion reuse
|
||||
@@ -904,6 +905,9 @@ jwtAuthorizationGrantMaxAllowedAssertionExpiration=Max allowed assertion expirat
|
||||
jwtAuthorizationGrantMaxAllowedAssertionExpirationHelp=Insert the max allowed expiration that the assertion can have.
|
||||
jwtAuthorizationGrantAssertionSignatureAlg=Assertion signature algorithm
|
||||
jwtAuthorizationGrantAssertionSignatureAlgHelp=Signature algorithm that should be used to sign the assertion, if not specified any signature algorithm will be valid.
|
||||
addJWTAuthorizationGrantProvider=Add JWT Authorization Grant Provider
|
||||
jwtAuthorizationGrantJWKSUrl=JWKS URL
|
||||
jwtAuthorizationGrantJWKSUrlHelp=URL where identity provider keys in JWK format are stored. See the JWK specification for more details
|
||||
userProviderSaveSuccess=User federation provider successfully saved
|
||||
month=Month
|
||||
valueLabel=Value
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
|
||||
import {
|
||||
ActionGroup,
|
||||
AlertVariant,
|
||||
Button,
|
||||
PageSection,
|
||||
} from "@patternfly/react-core";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useAdminClient } from "../../admin-client";
|
||||
import { useAlerts } from "@keycloak/keycloak-ui-shared";
|
||||
import { FormAccess } from "../../components/form/FormAccess";
|
||||
import { ViewHeader } from "../../components/view-header/ViewHeader";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { toIdentityProvider } from "../routes/IdentityProvider";
|
||||
import { toIdentityProviders } from "../routes/IdentityProviders";
|
||||
import JWTAuthorizationGrantSettings from "./JWTAuthorizationGrantSettings";
|
||||
type DiscoveryIdentityProvider = IdentityProviderRepresentation & {
|
||||
discoveryEndpoint?: string;
|
||||
};
|
||||
|
||||
export default function AddJWTAuthorizationGrantConnect() {
|
||||
const { adminClient } = useAdminClient();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const id = "jwt-authorization-grant";
|
||||
|
||||
const form = useForm<DiscoveryIdentityProvider>({
|
||||
defaultValues: { alias: id, config: { allowCreate: "true" } },
|
||||
mode: "onChange",
|
||||
});
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isDirty },
|
||||
} = form;
|
||||
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const { realm } = useRealm();
|
||||
|
||||
const onSubmit = async (provider: DiscoveryIdentityProvider) => {
|
||||
delete provider.discoveryEndpoint;
|
||||
try {
|
||||
await adminClient.identityProviders.create({
|
||||
...provider,
|
||||
providerId: id,
|
||||
});
|
||||
addAlert(t("createIdentityProviderSuccess"), AlertVariant.success);
|
||||
navigate(
|
||||
toIdentityProvider({
|
||||
realm,
|
||||
providerId: id,
|
||||
alias: provider.alias!,
|
||||
tab: "settings",
|
||||
}),
|
||||
);
|
||||
} catch (error: any) {
|
||||
addError("createIdentityProviderError", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeader titleKey={t("addJWTAuthorizationGrantProvider")} />
|
||||
<PageSection variant="light">
|
||||
<FormProvider {...form}>
|
||||
<FormAccess
|
||||
role="manage-identity-providers"
|
||||
isHorizontal
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<JWTAuthorizationGrantSettings />
|
||||
|
||||
<ActionGroup>
|
||||
<Button
|
||||
isDisabled={!isDirty}
|
||||
variant="primary"
|
||||
type="submit"
|
||||
data-testid="createProvider"
|
||||
>
|
||||
{t("add")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
data-testid="cancel"
|
||||
component={(props) => (
|
||||
<Link {...props} to={toIdentityProviders({ realm })} />
|
||||
)}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
</FormProvider>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
PageSection,
|
||||
Tab,
|
||||
TabTitleText,
|
||||
Text,
|
||||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
import { useMemo, useState } from "react";
|
||||
@@ -72,7 +73,9 @@ import { SpiffeSettings } from "./SpiffeSettings";
|
||||
import { AdminEvents } from "../../events/AdminEvents";
|
||||
import { UserProfileClaimsSettings } from "./OAuth2UserProfileClaimsSettings";
|
||||
import { KubernetesSettings } from "./KubernetesSettings";
|
||||
import { JwtAuthorizationGrantSettings } from "./JwtAuthorizationGrantSettings";
|
||||
import { JWTAuthorizationGrantAssertionSettings } from "./JWTAuthorizationGrantAssertionSettings";
|
||||
import JWTAuthorizationGrantSettings from "./JWTAuthorizationGrantSettings";
|
||||
import { DefaultSwitchControl } from "../../components/SwitchControl";
|
||||
|
||||
type HeaderProps = {
|
||||
onChange: (value: boolean) => void;
|
||||
@@ -263,7 +266,7 @@ export default function DetailSettings() {
|
||||
const { alias, providerId } = useParams<IdentityProviderParams>();
|
||||
const isFeatureEnabled = useIsFeatureEnabled();
|
||||
const form = useForm<IdentityProviderRepresentation>();
|
||||
const { handleSubmit, getValues, reset } = form;
|
||||
const { handleSubmit, getValues, reset, control } = form;
|
||||
const [provider, setProvider] = useState<IdentityProviderRepresentation>();
|
||||
const [selectedMapper, setSelectedMapper] =
|
||||
useState<IdPWithMapperAttributes>();
|
||||
@@ -409,6 +412,10 @@ export default function DetailSettings() {
|
||||
}
|
||||
},
|
||||
});
|
||||
const jwtAuthorizationGrantEnabled = useWatch({
|
||||
control,
|
||||
name: "config.jwtAuthorizationGrantEnabled",
|
||||
});
|
||||
|
||||
if (!provider) {
|
||||
return <KeycloakSpinner />;
|
||||
@@ -419,8 +426,11 @@ export default function DetailSettings() {
|
||||
const isOAuth2 = provider.providerId!.includes("oauth2");
|
||||
const isSPIFFE = provider.providerId!.includes("spiffe");
|
||||
const isKubernetes = provider.providerId!.includes("kubernetes");
|
||||
const isJWTAuthorizationGrant = provider.providerId!.includes(
|
||||
"jwt-authorization-grant",
|
||||
);
|
||||
const isSocial = !isOIDC && !isSAML && !isOAuth2;
|
||||
const isJwtAuthorizationGrantSupported =
|
||||
const isJWTAuthorizationGrantSupported =
|
||||
(isOAuth2 || isOIDC) &&
|
||||
!!provider?.types?.includes(IdentityProviderType.JWT_AUTHORIZATION_GRANT) &&
|
||||
isFeatureEnabled(Feature.JWTAuthorizationGrant);
|
||||
@@ -453,7 +463,7 @@ export default function DetailSettings() {
|
||||
const sections = [
|
||||
{
|
||||
title: t("generalSettings"),
|
||||
isHidden: isSPIFFE || isKubernetes,
|
||||
isHidden: isSPIFFE || isKubernetes || isJWTAuthorizationGrant,
|
||||
panel: (
|
||||
<FormAccess
|
||||
role="manage-identity-providers"
|
||||
@@ -500,15 +510,29 @@ export default function DetailSettings() {
|
||||
},
|
||||
{
|
||||
title: t("authorizationGrantSettings"),
|
||||
isHidden: !isJwtAuthorizationGrantSupported,
|
||||
isHidden: !isJWTAuthorizationGrantSupported,
|
||||
panel: (
|
||||
<Form
|
||||
isHorizontal
|
||||
className="pf-v5-u-py-lg"
|
||||
onSubmit={handleSubmit(save)}
|
||||
>
|
||||
<JwtAuthorizationGrantSettings />
|
||||
</Form>
|
||||
<>
|
||||
<Text className="pf-v5-u-pb-lg">
|
||||
{t("authorizationGrantSettingsHelp")}
|
||||
</Text>
|
||||
<Form
|
||||
isHorizontal
|
||||
className="pf-v5-u-py-lg"
|
||||
onSubmit={handleSubmit(save)}
|
||||
>
|
||||
<DefaultSwitchControl
|
||||
name="config.jwtAuthorizationGrantEnabled"
|
||||
label={t("jwtAuthorizationGrantIdpEnabled")}
|
||||
labelIcon={t("jwtAuthorizationGrantIdpEnabledHelp")}
|
||||
stringify
|
||||
/>
|
||||
|
||||
{jwtAuthorizationGrantEnabled === "true" && (
|
||||
<JWTAuthorizationGrantAssertionSettings />
|
||||
)}
|
||||
</Form>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -525,6 +549,20 @@ export default function DetailSettings() {
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("generalSettings"),
|
||||
isHidden: !isJWTAuthorizationGrant,
|
||||
panel: (
|
||||
<Form
|
||||
isHorizontal
|
||||
className="pf-v5-u-py-lg"
|
||||
onSubmit={handleSubmit(save)}
|
||||
>
|
||||
<JWTAuthorizationGrantSettings />
|
||||
<FixedButtonsGroup name="idp-details" isSubmit reset={reset} />
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("generalSettings"),
|
||||
isHidden: !isKubernetes,
|
||||
@@ -559,7 +597,7 @@ export default function DetailSettings() {
|
||||
},
|
||||
{
|
||||
title: t("advancedSettings"),
|
||||
isHidden: isSPIFFE || isKubernetes,
|
||||
isHidden: isSPIFFE || isKubernetes || isJWTAuthorizationGrant,
|
||||
panel: (
|
||||
<FormAccess
|
||||
role="manage-identity-providers"
|
||||
@@ -611,7 +649,7 @@ export default function DetailSettings() {
|
||||
</Tab>
|
||||
<Tab
|
||||
id="mappers"
|
||||
isHidden={isSPIFFE || isKubernetes}
|
||||
isHidden={isSPIFFE || isKubernetes || isJWTAuthorizationGrant}
|
||||
data-testid="mappers-tab"
|
||||
title={<TabTitleText>{t("mappers")}</TabTitleText>}
|
||||
{...mappersTab}
|
||||
|
||||
@@ -97,6 +97,7 @@ const Fields = ({ readOnly, isOIDC }: DiscoverySettingsProps) => {
|
||||
<DefaultSwitchControl
|
||||
name="config.useJwksUrl"
|
||||
label={t("useJwksUrl")}
|
||||
labelIcon={t("useJwksUrlHelp")}
|
||||
isDisabled={readOnly}
|
||||
stringify
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DefaultSwitchControl } from "../../components/SwitchControl";
|
||||
import { FormGroup } from "@patternfly/react-core";
|
||||
import { useFormContext, Controller } from "react-hook-form";
|
||||
import { TimeSelector } from "../../components/time-selector/TimeSelector";
|
||||
import { SelectControl, HelpItem } from "@keycloak/keycloak-ui-shared";
|
||||
import { sortProviders } from "../../util";
|
||||
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||
|
||||
export const JWTAuthorizationGrantAssertionSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const providers = useServerInfo().providers!.signature.providers;
|
||||
const { control } = useFormContext();
|
||||
return (
|
||||
<>
|
||||
<DefaultSwitchControl
|
||||
name="config.jwtAuthorizationGrantAssertionReuseAllowed"
|
||||
label={t("jwtAuthorizationGrantAssertionReuseAllowed")}
|
||||
labelIcon={t("jwtAuthorizationGrantAssertionReuseAllowedHelp")}
|
||||
stringify
|
||||
/>
|
||||
|
||||
<FormGroup
|
||||
label={t("jwtAuthorizationGrantMaxAllowedAssertionExpiration")}
|
||||
fieldId="jwtAuthorizationGrantMaxAllowedAssertionExpiration"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t(
|
||||
"jwtAuthorizationGrantMaxAllowedAssertionExpirationHelp",
|
||||
)}
|
||||
fieldLabelId="jwtAuthorizationGrantMaxAllowedAssertionExpirationHelp"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="config.jwtAuthorizationGrantMaxAllowedAssertionExpirationHelp"
|
||||
defaultValue={300}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TimeSelector
|
||||
data-testid="jwtAuthorizationGrantMaxAllowedAssertionExpirationHelp"
|
||||
value={field.value!}
|
||||
onChange={field.onChange}
|
||||
units={["second", "minute", "hour"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<SelectControl
|
||||
name="config.jwtAuthorizationGrantAssertionSignatureAlg"
|
||||
label={t("jwtAuthorizationGrantAssertionSignatureAlg")}
|
||||
labelIcon={t("jwtAuthorizationGrantAssertionSignatureAlgHelp")}
|
||||
options={[
|
||||
{ key: "", value: t("algorithmNotSpecified") },
|
||||
...sortProviders(providers).map((p) => ({ key: p, value: p })),
|
||||
]}
|
||||
controller={{
|
||||
defaultValue: "",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { TextControl, NumberControl } from "@keycloak/keycloak-ui-shared";
|
||||
import { JWTAuthorizationGrantAssertionSettings } from "./JWTAuthorizationGrantAssertionSettings";
|
||||
import { Divider } from "@patternfly/react-core";
|
||||
export default function JWTAuthorizationGrantSettings() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<TextControl
|
||||
name="alias"
|
||||
label={t("alias")}
|
||||
labelIcon={t("aliasHelp")}
|
||||
rules={{
|
||||
required: t("required"),
|
||||
}}
|
||||
/>
|
||||
<TextControl
|
||||
name="config.issuer"
|
||||
label={t("issuer")}
|
||||
rules={{
|
||||
required: t("required"),
|
||||
}}
|
||||
/>
|
||||
<TextControl
|
||||
name="config.jwksUrl"
|
||||
label={t("jwtAuthorizationGrantJWKSUrl")}
|
||||
labelIcon={t("jwtAuthorizationGrantJWKSUrlHelp")}
|
||||
rules={{
|
||||
required: t("required"),
|
||||
}}
|
||||
/>
|
||||
<JWTAuthorizationGrantAssertionSettings />
|
||||
<NumberControl
|
||||
name="config.jwtAuthorizationGrantAllowedClockSkew"
|
||||
label={t("allowedClockSkew")}
|
||||
labelIcon={t("allowedClockSkewHelp")}
|
||||
controller={{ defaultValue: 0, rules: { min: 0, max: 2147483 } }}
|
||||
/>
|
||||
<Divider />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DefaultSwitchControl } from "../../components/SwitchControl";
|
||||
import { Divider, FormGroup } from "@patternfly/react-core";
|
||||
import { useWatch, useFormContext, Controller } from "react-hook-form";
|
||||
import { TimeSelector } from "../../components/time-selector/TimeSelector";
|
||||
import { SelectControl, HelpItem } from "@keycloak/keycloak-ui-shared";
|
||||
import { sortProviders } from "../../util";
|
||||
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||
|
||||
export const JwtAuthorizationGrantSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const { control } = useFormContext();
|
||||
const authorizationGrantEnabled = useWatch({
|
||||
control,
|
||||
name: "config.jwtAuthorizationGrantEnabled",
|
||||
});
|
||||
const providers = useServerInfo().providers!.signature.providers;
|
||||
return (
|
||||
<>
|
||||
<DefaultSwitchControl
|
||||
name="config.jwtAuthorizationGrantEnabled"
|
||||
label={t("jwtAuthorizationGrantIdpEnabled")}
|
||||
labelIcon={t("jwtAuthorizationGrantIdpEnabledHelp")}
|
||||
stringify
|
||||
/>
|
||||
{authorizationGrantEnabled === "true" && (
|
||||
<>
|
||||
<DefaultSwitchControl
|
||||
name="config.jwtAuthorizationGrantAssertionReuseAllowed"
|
||||
label={t("jwtAuthorizationGrantAssertionReuseAllowed")}
|
||||
labelIcon={t("jwtAuthorizationGrantAssertionReuseAllowedHelp")}
|
||||
stringify
|
||||
/>
|
||||
|
||||
<FormGroup
|
||||
label={t("jwtAuthorizationGrantMaxAllowedAssertionExpiration")}
|
||||
fieldId="jwtAuthorizationGrantMaxAllowedAssertionExpiration"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t(
|
||||
"jwtAuthorizationGrantMaxAllowedAssertionExpirationHelp",
|
||||
)}
|
||||
fieldLabelId="jwtAuthorizationGrantMaxAllowedAssertionExpirationHelp"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="config.jwtAuthorizationGrantMaxAllowedAssertionExpirationHelp"
|
||||
defaultValue={300}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TimeSelector
|
||||
data-testid="jwtAuthorizationGrantMaxAllowedAssertionExpirationHelp"
|
||||
value={field.value!}
|
||||
onChange={field.onChange}
|
||||
units={["second", "minute", "hour"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<SelectControl
|
||||
name="config.jwtAuthorizationGrantAssertionSignatureAlg"
|
||||
label={t("jwtAuthorizationGrantAssertionSignatureAlg")}
|
||||
labelIcon={t("jwtAuthorizationGrantAssertionSignatureAlgHelp")}
|
||||
options={[
|
||||
{ key: "", value: t("algorithmNotSpecified") },
|
||||
...sortProviders(providers).map((p) => ({ key: p, value: p })),
|
||||
]}
|
||||
controller={{
|
||||
defaultValue: "",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Divider />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import { IdentityProviderAddMapperRoute } from "./routes/AddMapper";
|
||||
import { IdentityProviderEditMapperRoute } from "./routes/EditMapper";
|
||||
import { IdentityProviderCreateRoute } from "./routes/IdentityProviderCreate";
|
||||
import { IdentityProviderOAuth2Route } from "./routes/IdentityProviderOAuth2";
|
||||
import { IdentityProviderJWTAuthorizationGrantRoute } from "./routes/IdentityProviderJWTAuthorizationGrant";
|
||||
|
||||
const routes: AppRouteObject[] = [
|
||||
IdentityProviderAddMapperRoute,
|
||||
@@ -18,6 +19,7 @@ const routes: AppRouteObject[] = [
|
||||
IdentityProviderOidcRoute,
|
||||
IdentityProviderSamlRoute,
|
||||
IdentityProviderSpiffeRoute,
|
||||
IdentityProviderJWTAuthorizationGrantRoute,
|
||||
IdentityProviderKubernetesRoute,
|
||||
IdentityProviderKeycloakOidcRoute,
|
||||
IdentityProviderCreateRoute,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { lazy } from "react";
|
||||
import type { Path } from "react-router-dom";
|
||||
import { generateEncodedPath } from "../../utils/generateEncodedPath";
|
||||
import type { AppRouteObject } from "../../routes";
|
||||
|
||||
export type IdentityProviderJWTAuthorizationGrantParams = { realm: string };
|
||||
|
||||
const AddJWTAuthorizationGrant = lazy(
|
||||
() => import("../add/AddJWTAuthorizationGrant"),
|
||||
);
|
||||
|
||||
export const IdentityProviderJWTAuthorizationGrantRoute: AppRouteObject = {
|
||||
path: "/:realm/identity-providers/jwt-authorization-grant/add",
|
||||
element: <AddJWTAuthorizationGrant />,
|
||||
breadcrumb: (t) => t("addJWTAuthorizationGrantProvider"),
|
||||
handle: {
|
||||
access: "manage-identity-providers",
|
||||
},
|
||||
};
|
||||
|
||||
export const toIdentityProviderJWTAuthorizationGrant = (
|
||||
params: IdentityProviderJWTAuthorizationGrantParams,
|
||||
): Partial<Path> => ({
|
||||
pathname: generateEncodedPath(
|
||||
IdentityProviderJWTAuthorizationGrantRoute.path,
|
||||
params,
|
||||
),
|
||||
});
|
||||
@@ -25,6 +25,7 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.models.IdentityProviderCapability;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.IdentityProviderQuery;
|
||||
@@ -183,7 +184,7 @@ public class InfinispanIdentityProviderStorageProvider implements IdentityProvid
|
||||
|
||||
if (cached == null) {
|
||||
Long loaded = realmCache.getCache().getCurrentRevision(cacheKey);
|
||||
long count = idpDelegate.getAllStream(IdentityProviderQuery.userAuthentication(), 0, 1).count();
|
||||
long count = idpDelegate.getAllStream(IdentityProviderQuery.capability(IdentityProviderCapability.USER_LINKING), 0, 1).count();
|
||||
cached = new CachedCount(loaded, getRealm(), cacheKey, count);
|
||||
realmCache.getCache().addRevisioned(cached, realmCache.getStartupRevision());
|
||||
}
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
package org.keycloak.broker.provider;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.protocol.oidc.JWTAuthorizationGrantValidationContext;
|
||||
|
||||
public interface JWTAuthorizationGrantProvider {
|
||||
public interface JWTAuthorizationGrantProvider <C extends IdentityProviderModel> extends IdentityProvider<C> {
|
||||
|
||||
BrokeredIdentityContext validateAuthorizationGrantAssertion(JWTAuthorizationGrantValidationContext assertion) throws IdentityBrokerException;
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ public interface IdentityProviderStorageProvider extends Provider {
|
||||
* otherwise.
|
||||
*/
|
||||
default boolean isIdentityFederationEnabled() {
|
||||
return getAllStream(IdentityProviderQuery.userAuthentication(), 0, 1).findFirst().isPresent();
|
||||
return getAllStream(IdentityProviderQuery.capability(IdentityProviderCapability.USER_LINKING), 0, 1).findFirst().isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.keycloak.broker.jwtauthorizationgrant;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.keycloak.broker.oidc.OIDCIdentityProviderConfig.JWKS_URL;
|
||||
import static org.keycloak.protocol.oidc.OIDCLoginProtocol.ISSUER;
|
||||
|
||||
public interface JWTAuthorizationGrantConfig {
|
||||
|
||||
public static final String JWT_AUTHORIZATION_GRANT_ENABLED = "jwtAuthorizationGrantEnabled";
|
||||
|
||||
public static final String JWT_AUTHORIZATION_GRANT_ASSERTION_REUSE_ALLOWED = "jwtAuthorizationGrantAssertionReuseAllowed";
|
||||
|
||||
public static final String JWT_AUTHORIZATION_GRANT_MAX_ALLOWED_ASSERTION_EXPIRATION = "jwtAuthorizationGrantMaxAllowedAssertionExpiration";
|
||||
|
||||
public static final String JWT_AUTHORIZATION_GRANT_ASSERTION_SIGNATURE_ALG = "jwtAuthorizationGrantAssertionSignatureAlg";
|
||||
|
||||
public static final String JWT_AUTHORIZATION_GRANT_ALLOWED_CLOCK_SKEW = "jwtAuthorizationGrantAllowedClockSkew";
|
||||
|
||||
Map<String, String> getConfig();
|
||||
|
||||
default boolean getJWTAuthorizationGrantEnabled() {
|
||||
return Boolean.parseBoolean(getConfig().getOrDefault(JWT_AUTHORIZATION_GRANT_ENABLED, "false"));
|
||||
}
|
||||
|
||||
default boolean getJWTAuthorizationGrantAssertionReuseAllowed() {
|
||||
return Boolean.parseBoolean(getConfig().getOrDefault(JWT_AUTHORIZATION_GRANT_ASSERTION_REUSE_ALLOWED, "false"));
|
||||
}
|
||||
|
||||
default int getJWTAuthorizationGrantMaxAllowedAssertionExpiration() {
|
||||
return Integer.parseInt(getConfig().getOrDefault(JWT_AUTHORIZATION_GRANT_MAX_ALLOWED_ASSERTION_EXPIRATION, "300"));
|
||||
}
|
||||
|
||||
default String getJWTAuthorizationGrantAssertionSignatureAlg() {
|
||||
return getConfig().get(JWT_AUTHORIZATION_GRANT_ASSERTION_SIGNATURE_ALG);
|
||||
}
|
||||
|
||||
default int getJWTAuthorizationGrantAllowedClockSkew() {
|
||||
String allowedClockSkew = getConfig().get(JWT_AUTHORIZATION_GRANT_ALLOWED_CLOCK_SKEW);
|
||||
if (allowedClockSkew == null || allowedClockSkew.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(getConfig().get(JWT_AUTHORIZATION_GRANT_ALLOWED_CLOCK_SKEW));
|
||||
} catch (NumberFormatException e) {
|
||||
// ignore it and use default
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
default String getIssuer() {
|
||||
return getConfig().get(ISSUER);
|
||||
}
|
||||
|
||||
default String getJwksUrl() {
|
||||
return getConfig().get(JWKS_URL);
|
||||
}
|
||||
|
||||
String getInternalId();
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package org.keycloak.broker.jwtauthorizationgrant;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.broker.provider.JWTAuthorizationGrantProvider;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.crypto.SignatureProvider;
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.keys.PublicKeyStorageProvider;
|
||||
import org.keycloak.keys.PublicKeyStorageUtils;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oidc.JWTAuthorizationGrantValidationContext;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
public class JWTAuthorizationGrantIdentityProvider implements JWTAuthorizationGrantProvider<JWTAuthorizationGrantIdentityProviderConfig> {
|
||||
private static final Logger LOGGER = Logger.getLogger(JWTAuthorizationGrantIdentityProvider.class);
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final JWTAuthorizationGrantConfig config;
|
||||
|
||||
public JWTAuthorizationGrantIdentityProvider(KeycloakSession session, JWTAuthorizationGrantConfig config) {
|
||||
this.session = session;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BrokeredIdentityContext validateAuthorizationGrantAssertion(JWTAuthorizationGrantValidationContext context) throws IdentityBrokerException {
|
||||
// verify signature
|
||||
if (!verifySignature(context.getJws())) {
|
||||
throw new IdentityBrokerException("Invalid signature");
|
||||
}
|
||||
|
||||
BrokeredIdentityContext user = new BrokeredIdentityContext(context.getJWT().getSubject(), getConfig());
|
||||
user.setUsername(context.getJWT().getSubject());
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAllowedClockSkew() {
|
||||
return config.getJWTAuthorizationGrantAllowedClockSkew();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAssertionReuseAllowed() {
|
||||
return config.getJWTAuthorizationGrantAssertionReuseAllowed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAllowedAudienceForJWTGrant() {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
URI baseUri = session.getContext().getUri().getBaseUri();
|
||||
String issuer = Urls.realmIssuer(baseUri, realm.getName());
|
||||
String tokenEndpoint = Urls.tokenEndpoint(baseUri, realm.getName()).toString();
|
||||
return List.of(issuer, tokenEndpoint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxAllowedExpiration() {
|
||||
return config.getJWTAuthorizationGrantMaxAllowedAssertionExpiration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAssertionSignatureAlg() {
|
||||
String alg = config.getJWTAuthorizationGrantAssertionSignatureAlg();
|
||||
return StringUtil.isBlank(alg) ? null : alg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JWTAuthorizationGrantIdentityProviderConfig getConfig() {
|
||||
return this.config instanceof JWTAuthorizationGrantIdentityProviderConfig ? (JWTAuthorizationGrantIdentityProviderConfig)this.config : null;
|
||||
}
|
||||
|
||||
private boolean verifySignature(JWSInput jws) {
|
||||
try {
|
||||
String jwkurl = config.getJwksUrl();
|
||||
JWSHeader header = jws.getHeader();
|
||||
String kid = header.getKeyId();
|
||||
String alg = header.getRawAlgorithm();
|
||||
String modelKey = PublicKeyStorageUtils.getIdpModelCacheKey(session.getContext().getRealm().getId(), config.getInternalId());
|
||||
|
||||
PublicKeyStorageProvider keyStorage = session.getProvider(PublicKeyStorageProvider.class);
|
||||
KeyWrapper publicKey = keyStorage.getPublicKey(modelKey, kid, alg, new JWTAuthorizationGrantJWKSEndpointLoader(session, jwkurl));
|
||||
|
||||
SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, alg);
|
||||
if (signatureProvider == null) {
|
||||
LOGGER.debugf("Failed to verify token signature, signature provider not found for algorithm %s", alg);
|
||||
return false;
|
||||
}
|
||||
|
||||
return signatureProvider.verifier(publicKey).verify(jws.getEncodedSignatureInput().getBytes(StandardCharsets.UTF_8), jws.getSignature());
|
||||
} catch (Exception e) {
|
||||
LOGGER.debug("Failed to verify token signature", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.keycloak.broker.jwtauthorizationgrant;
|
||||
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
||||
import static org.keycloak.broker.oidc.OIDCIdentityProviderConfig.JWKS_URL;
|
||||
import static org.keycloak.common.util.UriUtils.checkUrl;
|
||||
|
||||
public class JWTAuthorizationGrantIdentityProviderConfig extends IdentityProviderModel implements JWTAuthorizationGrantConfig {
|
||||
|
||||
public JWTAuthorizationGrantIdentityProviderConfig() {
|
||||
}
|
||||
|
||||
public JWTAuthorizationGrantIdentityProviderConfig(IdentityProviderModel model) {
|
||||
super(model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(RealmModel realm) {
|
||||
checkUrl(realm.getSslRequired(), getIssuer(), ISSUER);
|
||||
checkUrl(realm.getSslRequired(), getJwksUrl(), JWKS_URL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.keycloak.broker.jwtauthorizationgrant;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
|
||||
public class JWTAuthorizationGrantIdentityProviderFactory extends AbstractIdentityProviderFactory<JWTAuthorizationGrantIdentityProvider> implements EnvironmentDependentProviderFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "jwt-authorization-grant";
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "JWT Authorization Grant";
|
||||
}
|
||||
|
||||
@Override
|
||||
public JWTAuthorizationGrantIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
|
||||
return new JWTAuthorizationGrantIdentityProvider(session, new JWTAuthorizationGrantIdentityProviderConfig(model));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> parseConfig(KeycloakSession session, String configString) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityProviderModel createConfig() {
|
||||
return new JWTAuthorizationGrantIdentityProviderConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported(Config.Scope config) {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.JWT_AUTHORIZATION_GRANT);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.keycloak.broker.jwtauthorizationgrant;
|
||||
|
||||
import org.keycloak.crypto.PublicKeysWrapper;
|
||||
import org.keycloak.http.simple.SimpleHttp;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.keys.PublicKeyLoader;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.util.JWKSUtils;
|
||||
|
||||
|
||||
public class JWTAuthorizationGrantJWKSEndpointLoader implements PublicKeyLoader {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final String jwksUrl;
|
||||
|
||||
public JWTAuthorizationGrantJWKSEndpointLoader(KeycloakSession session, String jwksUrl) {
|
||||
this.session = session;
|
||||
this.jwksUrl = jwksUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKeysWrapper loadKeys() throws Exception {
|
||||
JSONWebKeySet jwks = SimpleHttp.create(session).doGet(jwksUrl).asJson(JSONWebKeySet.class);
|
||||
return JWKSUtils.getKeyWrappersForUse(jwks, JWK.Use.SIG, true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -41,14 +41,6 @@ public class OAuth2IdentityProviderConfig extends IdentityProviderModel {
|
||||
|
||||
public static final String REQUIRES_SHORT_STATE_PARAMETER = "requiresShortStateParameter";
|
||||
|
||||
public static final String JWT_AUTHORIZATION_GRANT_ENABLED = "jwtAuthorizationGrantEnabled";
|
||||
|
||||
public static final String JWT_AUTHORIZATION_GRANT_ASSERTION_REUSE_ALLOWED = "jwtAuthorizationGrantAssertionReuseAllowed";
|
||||
|
||||
public static final String JWT_AUTHORIZATION_GRANT_MAX_ALLOWED_ASSERTION_EXPIRATION = "jwtAuthorizationGrantMaxAllowedAssertionExpiration";
|
||||
|
||||
public static final String JWT_AUTHORIZATION_GRANT_ASSERTION_SIGNATURE_ALG = "jwtAuthorizationGrantAssertionSignatureAlg";
|
||||
|
||||
public OAuth2IdentityProviderConfig(IdentityProviderModel model) {
|
||||
super(model);
|
||||
}
|
||||
@@ -169,22 +161,6 @@ public class OAuth2IdentityProviderConfig extends IdentityProviderModel {
|
||||
return Boolean.parseBoolean(getConfig().getOrDefault(PKCE_ENABLED, "false"));
|
||||
}
|
||||
|
||||
public boolean getJwtAuthorizationGrantEnabled() {
|
||||
return Boolean.parseBoolean(getConfig().getOrDefault(JWT_AUTHORIZATION_GRANT_ENABLED, "false"));
|
||||
}
|
||||
|
||||
public boolean getJwtAuthorizationGrantAssertionReuseAllowed() {
|
||||
return Boolean.parseBoolean(getConfig().getOrDefault(JWT_AUTHORIZATION_GRANT_ASSERTION_REUSE_ALLOWED, "false"));
|
||||
}
|
||||
|
||||
public int getJwtAuthorizationGrantMaxAllowedAssertionExpiration() {
|
||||
return Integer.parseInt(getConfig().getOrDefault(JWT_AUTHORIZATION_GRANT_MAX_ALLOWED_ASSERTION_EXPIRATION, "300"));
|
||||
}
|
||||
|
||||
public String getJwtAuthorizationGrantAssertionSignatureAlg() {
|
||||
return getConfig().get(JWT_AUTHORIZATION_GRANT_ASSERTION_SIGNATURE_ALG);
|
||||
}
|
||||
|
||||
public void setPkceEnabled(boolean enabled) {
|
||||
getConfig().put(PKCE_ENABLED, String.valueOf(enabled));
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package org.keycloak.broker.oidc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -40,6 +39,7 @@ import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.authentication.ClientAuthenticationFlowContext;
|
||||
import org.keycloak.authentication.authenticators.client.FederatedJWTClientValidator;
|
||||
import org.keycloak.broker.jwtauthorizationgrant.JWTAuthorizationGrantIdentityProvider;
|
||||
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
|
||||
import org.keycloak.broker.provider.AuthenticationRequest;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
@@ -84,7 +84,6 @@ import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.services.ErrorPage;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.IdentityBrokerService;
|
||||
@@ -93,7 +92,6 @@ import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.util.Booleans;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
import org.keycloak.vault.VaultStringSecret;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
@@ -102,7 +100,7 @@ import org.jboss.logging.Logger;
|
||||
/**
|
||||
* @author Pedro Igor
|
||||
*/
|
||||
public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIdentityProviderConfig> implements ExchangeExternalToken, ClientAssertionIdentityProvider<OIDCIdentityProviderConfig>, JWTAuthorizationGrantProvider {
|
||||
public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIdentityProviderConfig> implements ExchangeExternalToken, ClientAssertionIdentityProvider<OIDCIdentityProviderConfig>, JWTAuthorizationGrantProvider<OIDCIdentityProviderConfig> {
|
||||
protected static final Logger logger = Logger.getLogger(OIDCIdentityProvider.class);
|
||||
|
||||
public static final String SCOPE_OPENID = "openid";
|
||||
@@ -1074,9 +1072,8 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
return validator.validate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BrokeredIdentityContext validateAuthorizationGrantAssertion(JWTAuthorizationGrantValidationContext context) throws IdentityBrokerException {
|
||||
if (!getConfig().getJwtAuthorizationGrantEnabled()) {
|
||||
if (!getConfig().getJWTAuthorizationGrantEnabled()) {
|
||||
throw new IdentityBrokerException("JWT Authorization Granted is not enabled for the identity provider");
|
||||
}
|
||||
|
||||
@@ -1089,7 +1086,6 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
user.setUsername(context.getJWT().getSubject());
|
||||
user.setIdp(this);
|
||||
return user;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1099,27 +1095,21 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
|
||||
@Override
|
||||
public boolean isAssertionReuseAllowed() {
|
||||
return getConfig().getJwtAuthorizationGrantAssertionReuseAllowed();
|
||||
return getConfig().getJWTAuthorizationGrantAssertionReuseAllowed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAllowedAudienceForJWTGrant() {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
URI baseUri = session.getContext().getUri().getBaseUri();
|
||||
String issuer = Urls.realmIssuer(baseUri, realm.getName());
|
||||
String tokenEndpoint = Urls.tokenEndpoint(baseUri, realm.getName()).toString();
|
||||
return List.of(issuer, tokenEndpoint);
|
||||
return new JWTAuthorizationGrantIdentityProvider(session, getConfig()).getAllowedAudienceForJWTGrant();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxAllowedExpiration() {
|
||||
return getConfig().getJwtAuthorizationGrantMaxAllowedAssertionExpiration();
|
||||
return getConfig().getJWTAuthorizationGrantMaxAllowedAssertionExpiration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAssertionSignatureAlg() {
|
||||
String alg = getConfig().getJwtAuthorizationGrantAssertionSignatureAlg();
|
||||
return StringUtil.isBlank(alg) ? null : alg;
|
||||
return getConfig().getJWTAuthorizationGrantAssertionSignatureAlg();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.keycloak.broker.oidc;
|
||||
|
||||
import org.keycloak.broker.jwtauthorizationgrant.JWTAuthorizationGrantConfig;
|
||||
import org.keycloak.common.enums.SslRequired;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
@@ -25,7 +26,7 @@ import static org.keycloak.common.util.UriUtils.checkUrl;
|
||||
/**
|
||||
* @author Pedro Igor
|
||||
*/
|
||||
public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
|
||||
public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig implements JWTAuthorizationGrantConfig {
|
||||
|
||||
public static final String JWKS_URL = "jwksUrl";
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.JWTAuthorizationGrantProvider;
|
||||
import org.keycloak.broker.provider.UserAuthenticationIdentityProvider;
|
||||
import org.keycloak.cache.AlternativeLookupProvider;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
@@ -77,8 +76,8 @@ public class JWTAuthorizationGrantType extends OAuth2GrantTypeBase {
|
||||
throw new RuntimeException("Identity Provider is not allowed for the client");
|
||||
}
|
||||
|
||||
UserAuthenticationIdentityProvider<?> identityProvider = IdentityBrokerService.getIdentityProvider(session, identityProviderModel.getAlias());
|
||||
if (!(identityProvider instanceof JWTAuthorizationGrantProvider jwtAuthorizationGrantProvider)) {
|
||||
JWTAuthorizationGrantProvider jwtAuthorizationGrantProvider = IdentityBrokerService.getIdentityProvider(session, identityProviderModel, JWTAuthorizationGrantProvider.class);
|
||||
if (jwtAuthorizationGrantProvider == null) {
|
||||
throw new RuntimeException("Identity Provider is not configured for JWT Authorization Grant");
|
||||
}
|
||||
|
||||
|
||||
@@ -20,4 +20,5 @@ org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory
|
||||
org.keycloak.broker.saml.SAMLIdentityProviderFactory
|
||||
org.keycloak.broker.oauth.OAuth2IdentityProviderFactory
|
||||
org.keycloak.broker.spiffe.SpiffeIdentityProviderFactory
|
||||
org.keycloak.broker.kubernetes.KubernetesIdentityProviderFactory
|
||||
org.keycloak.broker.kubernetes.KubernetesIdentityProviderFactory
|
||||
org.keycloak.broker.jwtauthorizationgrant.JWTAuthorizationGrantIdentityProviderFactory
|
||||
@@ -12,7 +12,7 @@ public class DefaultOAuthClientConfiguration implements ClientConfig {
|
||||
.serviceAccountsEnabled(true)
|
||||
.directAccessGrantsEnabled(true)
|
||||
.attribute(OIDCConfigAttributes.JWT_AUTHORIZATION_GRANT_ENABLED, "true")
|
||||
.attribute(OIDCConfigAttributes.JWT_AUTHORIZATION_GRANT_IDP, "authorization-grant-idp")
|
||||
.attribute(OIDCConfigAttributes.JWT_AUTHORIZATION_GRANT_IDP, "authorization-grant-idp-alias")
|
||||
.secret("test-secret");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
package org.keycloak.tests.oauth;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectEvents;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.InjectUser;
|
||||
import org.keycloak.testframework.events.EventAssertion;
|
||||
import org.keycloak.testframework.events.Events;
|
||||
import org.keycloak.testframework.oauth.OAuthClient;
|
||||
import org.keycloak.testframework.oauth.OAuthIdentityProvider;
|
||||
import org.keycloak.testframework.oauth.OAuthIdentityProviderConfig;
|
||||
import org.keycloak.testframework.oauth.OAuthIdentityProviderConfigBuilder;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthIdentityProvider;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.ManagedUser;
|
||||
import org.keycloak.testframework.realm.RealmConfig;
|
||||
import org.keycloak.testframework.realm.RealmConfigBuilder;
|
||||
import org.keycloak.testframework.realm.UserConfig;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.remote.timeoffset.InjectTimeOffSet;
|
||||
import org.keycloak.testframework.remote.timeoffset.TimeOffSet;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
|
||||
import org.keycloak.tests.client.authentication.external.ClientAuthIdpServerConfig;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public abstract class AbstractJWTAuthorizationGrantTest {
|
||||
|
||||
public static String IDP_ALIAS = "authorization-grant-idp-alias";
|
||||
public static final String IDP_ISSUER = "https://authorization-grant-issuer";
|
||||
|
||||
@InjectOAuthIdentityProvider(config = AbstractJWTAuthorizationGrantTest.AGIdpConfig.class)
|
||||
OAuthIdentityProvider identityProvider;
|
||||
|
||||
@InjectRealm(config = JWTAuthorizationGrantRealmConfig.class)
|
||||
protected ManagedRealm realm;
|
||||
|
||||
@InjectUser(config = FederatedUserConfiguration.class)
|
||||
ManagedUser user;
|
||||
|
||||
@InjectOAuthClient
|
||||
OAuthClient oAuthClient;
|
||||
|
||||
@InjectEvents
|
||||
Events events;
|
||||
|
||||
@InjectTimeOffSet
|
||||
TimeOffSet timeOffSet;
|
||||
|
||||
@Test
|
||||
public void testPublicClient() {
|
||||
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
oAuthClient.client("test-public");
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Public client not allowed to use authorization grant", response, events.poll());
|
||||
oAuthClient.client("test-app", "test-secret");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIdpNotAllowedForClient() {
|
||||
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
oAuthClient.client("authorization-grant-not-allowed-idp-client", "test-secret");
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Identity Provider is not allowed for the client", response, events.poll());
|
||||
oAuthClient.client("test-app", "test-secret");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotAllowedClient() {
|
||||
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
oAuthClient.client("authorization-grant-disabled-client", "test-secret");
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("JWT Authorization Grant is not supported for the requested client", response, events.poll());
|
||||
oAuthClient.client("test-app", "test-secret");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingAssertionParameter() {
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(null).send();
|
||||
assertFailure("Missing parameter:" + OAuth2Constants.ASSERTION, response, events.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadAssertionParameter() {
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest("fake-jwt").send();
|
||||
assertFailure("The provided assertion is not a valid JWT", response, events.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExpiredAssertion() {
|
||||
String jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, null));
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Token exp claim is required", response, events.poll());
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() - 1L));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Token is not active", response, events.poll());
|
||||
|
||||
//test max exp default settings
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 301L));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Token expiration is too far in the future and iat claim not present in token", response, events.poll());
|
||||
|
||||
//reduce max expiration to 10 seconds
|
||||
realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> {
|
||||
rep.getConfig().put(OIDCIdentityProviderConfig.JWT_AUTHORIZATION_GRANT_MAX_ALLOWED_ASSERTION_EXPIRATION, "10");
|
||||
});
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 11L));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Token expiration is too far in the future and iat claim not present in token", response, events.poll());
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 5L));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
|
||||
//test with iat
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 20L, (long) Time.currentTime()));
|
||||
timeOffSet.set(15);
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Token was issued too far in the past to be used now", response, events.poll());
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 20L, (long) Time.currentTime()));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAudience() {
|
||||
String jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", null, IDP_ISSUER, Time.currentTime() + 300L));
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Invalid token audience", response, events.poll());
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", "fake-audience", IDP_ISSUER));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Invalid token audience", response, events.poll());
|
||||
|
||||
// Issuer as audience works
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
|
||||
// Token endpoint as audience works
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getToken(), IDP_ISSUER));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
|
||||
// Introspection endpoint as audience does not work
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIntrospection(), IDP_ISSUER));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Invalid token audience", response, events.poll());
|
||||
|
||||
// Multiple audiences does not work
|
||||
JsonWebToken jwtToken = createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER);
|
||||
jwtToken.addAudience("fake");
|
||||
jwt = getIdentityProvider().encodeToken(jwtToken);
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Multiple audiences not allowed", response, events.poll());
|
||||
|
||||
// Multiple audiences does not work (even if both are valid)
|
||||
jwtToken = createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER);
|
||||
jwtToken.addAudience(oAuthClient.getEndpoints().getToken());
|
||||
jwt = getIdentityProvider().encodeToken(jwtToken);
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Multiple audiences not allowed", response, events.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadIssuer() {
|
||||
String jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), null, Time.currentTime() + 300L));
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Missing claim: " + OAuth2Constants.ISSUER, response, events.poll());
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), "fake-issuer", Time.currentTime() + 300L));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("No Identity Provider for provided issuer", response, events.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadSubject() {
|
||||
String jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken(null, oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 300L));
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Missing claim: " + IDToken.SUBJECT, response, events.poll());
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("fake-user", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 300L));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("User not found", response, events.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReplayToken() {
|
||||
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Token reuse detected", response, events.poll());
|
||||
|
||||
realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> {
|
||||
rep.getConfig().put(OIDCIdentityProviderConfig.JWT_AUTHORIZATION_GRANT_ASSERTION_REUSE_ALLOWED, "true");
|
||||
});
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignatureAlg() {
|
||||
realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> {
|
||||
rep.getConfig().put(OIDCIdentityProviderConfig.JWT_AUTHORIZATION_GRANT_ASSERTION_SIGNATURE_ALG, Algorithm.ES256);
|
||||
});
|
||||
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
|
||||
realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> {
|
||||
rep.getConfig().put(OIDCIdentityProviderConfig.JWT_AUTHORIZATION_GRANT_ASSERTION_SIGNATURE_ALG, Algorithm.ES512);
|
||||
});
|
||||
jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Invalid signature algorithm", response, events.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidSignature() {
|
||||
JsonWebToken token = createDefaultAuthorizationGrantToken();
|
||||
OAuthIdentityProvider.OAuthIdentityProviderKeys newKeys = getIdentityProvider().createKeys();
|
||||
OAuthIdentityProvider.OAuthIdentityProviderKeys keys = getIdentityProvider().getKeys();
|
||||
newKeys.getKeyWrapper().setKid(keys.getKeyWrapper().getKid());
|
||||
String jwt = getIdentityProvider().encodeToken(token, newKeys);
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Invalid signature", response, events.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScope() {
|
||||
oAuthClient.openid(false).scope("address phone");
|
||||
try {
|
||||
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
AccessToken token = assertSuccess("test-app", "basic-user", response);
|
||||
MatcherAssert.assertThat(List.of(token.getScope().split(" ")), Matchers.containsInAnyOrder(new String[]{"email", "profile", "address", "phone"}));
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
oAuthClient.scope("address phone wrong-scope");
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("invalid_scope", "Invalid scopes: address phone wrong-scope", response, events.poll());
|
||||
} finally {
|
||||
oAuthClient.openid(true).scope(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessGrant() {
|
||||
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
}
|
||||
|
||||
protected JsonWebToken createDefaultAuthorizationGrantToken() {
|
||||
return createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 300L);
|
||||
}
|
||||
|
||||
protected JsonWebToken createAuthorizationGrantToken(String subject, String audience, String issuer) {
|
||||
return createAuthorizationGrantToken(subject, audience, issuer, Time.currentTime() + 300L, (long) Time.currentTime());
|
||||
}
|
||||
|
||||
protected JsonWebToken createAuthorizationGrantToken(String subject, String audience, String issuer, Long exp) {
|
||||
return createAuthorizationGrantToken(subject, audience, issuer, exp, null);
|
||||
}
|
||||
|
||||
protected JsonWebToken createAuthorizationGrantToken(String subject, String audience, String issuer, Long exp, Long iat) {
|
||||
JsonWebToken token = new JsonWebToken();
|
||||
token.id(UUID.randomUUID().toString());
|
||||
token.subject(subject);
|
||||
token.audience(audience);
|
||||
token.issuer(issuer);
|
||||
token.exp(exp);
|
||||
token.iat(iat);
|
||||
return token;
|
||||
}
|
||||
|
||||
public OAuthIdentityProvider getIdentityProvider() {
|
||||
return identityProvider;
|
||||
}
|
||||
|
||||
public static class AGIdpConfig implements OAuthIdentityProviderConfig {
|
||||
|
||||
@Override
|
||||
public OAuthIdentityProviderConfigBuilder configure(OAuthIdentityProviderConfigBuilder config) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
public static class JWTAuthorizationGrantServerConfig extends ClientAuthIdpServerConfig {
|
||||
|
||||
@Override
|
||||
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
|
||||
return super.configure(config).features(Profile.Feature.JWT_AUTHORIZATION_GRANT);
|
||||
}
|
||||
}
|
||||
|
||||
public static class JWTAuthorizationGrantRealmConfig implements RealmConfig {
|
||||
|
||||
@Override
|
||||
public RealmConfigBuilder configure(RealmConfigBuilder realm) {
|
||||
realm.addClient("test-public").publicClient(true);
|
||||
realm.addClient("authorization-grant-disabled-client").publicClient(false).secret("test-secret");
|
||||
realm.addClient("authorization-grant-not-allowed-idp-client").publicClient(false).attribute(OIDCConfigAttributes.JWT_AUTHORIZATION_GRANT_ENABLED, "true").secret("test-secret");
|
||||
return realm;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FederatedUserConfiguration implements UserConfig {
|
||||
|
||||
@Override
|
||||
public UserConfigBuilder configure(UserConfigBuilder user) {
|
||||
return user
|
||||
.username("basic-user")
|
||||
.password("password")
|
||||
.email("basic@localhost")
|
||||
.name("First", "Last")
|
||||
.federatedLink(IDP_ALIAS, "basic-user-id", "basic-user");
|
||||
}
|
||||
}
|
||||
|
||||
protected AccessToken assertSuccess(String expectedClientId, String username, AccessTokenResponse response) {
|
||||
Assertions.assertTrue(response.isSuccess());
|
||||
Assertions.assertNull(response.getRefreshToken());
|
||||
AccessToken accessToken = oAuthClient.parseToken(response.getAccessToken(), AccessToken.class);
|
||||
Assertions.assertNull(accessToken.getSessionId());
|
||||
MatcherAssert.assertThat(accessToken.getId(), Matchers.startsWith("trrtag:"));
|
||||
Assertions.assertEquals(expectedClientId, accessToken.getIssuedFor());
|
||||
Assertions.assertEquals(username, accessToken.getPreferredUsername());
|
||||
EventAssertion.assertSuccess(events.poll())
|
||||
.type(EventType.LOGIN)
|
||||
.clientId(expectedClientId)
|
||||
.details("grant_type", OAuth2Constants.JWT_AUTHORIZATION_GRANT)
|
||||
.details("username", username);
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
protected void assertFailure(String expectedErrorDescription, AccessTokenResponse response, EventRepresentation event) {
|
||||
assertFailure("invalid_grant", expectedErrorDescription, response, event);
|
||||
}
|
||||
|
||||
protected void assertFailure(String expectedError, String expectedErrorDescription, AccessTokenResponse response, EventRepresentation event) {
|
||||
Assertions.assertFalse(response.isSuccess());
|
||||
Assertions.assertEquals(expectedError, response.getError());
|
||||
Assertions.assertEquals(expectedErrorDescription, response.getErrorDescription());
|
||||
EventAssertion.assertError(event)
|
||||
.type(EventType.LOGIN_ERROR)
|
||||
.error("invalid_request")
|
||||
.details("grant_type", OAuth2Constants.JWT_AUTHORIZATION_GRANT)
|
||||
.details("reason", expectedErrorDescription);
|
||||
}
|
||||
}
|
||||
@@ -1,404 +1,36 @@
|
||||
package org.keycloak.tests.oauth;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.broker.jwtauthorizationgrant.JWTAuthorizationGrantIdentityProviderFactory;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectEvents;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.InjectUser;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.events.EventAssertion;
|
||||
import org.keycloak.testframework.events.Events;
|
||||
import org.keycloak.testframework.oauth.OAuthClient;
|
||||
import org.keycloak.testframework.oauth.OAuthIdentityProvider;
|
||||
import org.keycloak.testframework.oauth.OAuthIdentityProviderConfig;
|
||||
import org.keycloak.testframework.oauth.OAuthIdentityProviderConfigBuilder;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthIdentityProvider;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.ManagedUser;
|
||||
import org.keycloak.testframework.realm.RealmConfig;
|
||||
import org.keycloak.testframework.realm.RealmConfigBuilder;
|
||||
import org.keycloak.testframework.realm.UserConfig;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.remote.timeoffset.InjectTimeOffSet;
|
||||
import org.keycloak.testframework.remote.timeoffset.TimeOffSet;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
|
||||
import org.keycloak.tests.client.authentication.external.ClientAuthIdpServerConfig;
|
||||
import org.keycloak.testsuite.util.IdentityProviderBuilder;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
@KeycloakIntegrationTest(config = JWTAuthorizationGrantTest.JWTAuthorizationGrantServerConfig.class)
|
||||
public class JWTAuthorizationGrantTest {
|
||||
@TestMethodOrder(MethodOrderer.MethodName.class)
|
||||
public class JWTAuthorizationGrantTest extends AbstractJWTAuthorizationGrantTest {
|
||||
|
||||
private static final String IDP_ALIAS = "authorization-grant-idp";
|
||||
private static final String IDP_ISSUER = "authorization-grant://mytrust-domain";
|
||||
|
||||
@InjectOAuthIdentityProvider(config = JWTAuthorizationGrantTest.AGIdpConfig.class)
|
||||
OAuthIdentityProvider identityProvider;
|
||||
|
||||
@InjectRealm(config = JWTAuthorizationGranthRealmConfig.class)
|
||||
@InjectRealm(config = JWTAuthorizationGrantTest.JWTAuthorizationGrantRealmConfig.class)
|
||||
protected ManagedRealm realm;
|
||||
|
||||
@InjectUser(config = FederatedUserConfiguration.class)
|
||||
ManagedUser user;
|
||||
|
||||
@InjectOAuthClient
|
||||
OAuthClient oAuthClient;
|
||||
|
||||
@InjectEvents
|
||||
Events events;
|
||||
|
||||
@InjectTimeOffSet
|
||||
TimeOffSet timeOffSet;
|
||||
|
||||
@Test
|
||||
public void testPublicClient() {
|
||||
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
oAuthClient.client("test-public");
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Public client not allowed to use authorization grant", response, events.poll());
|
||||
oAuthClient.client("test-app", "test-secret");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIdpNotAllowedForClient() {
|
||||
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
oAuthClient.client("authorization-grant-not-allowed-idp-client", "test-secret");
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Identity Provider is not allowed for the client", response, events.poll());
|
||||
oAuthClient.client("test-app", "test-secret");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotAllowedIdentityProvider() {
|
||||
realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> {
|
||||
rep.getConfig().put(OIDCIdentityProviderConfig.JWT_AUTHORIZATION_GRANT_ENABLED, "false");
|
||||
});
|
||||
|
||||
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("JWT Authorization Granted is not enabled for the identity provider", response, events.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotAllowedClient() {
|
||||
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
oAuthClient.client("authorization-grant-disabled-client", "test-secret");
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("JWT Authorization Grant is not supported for the requested client", response, events.poll());
|
||||
oAuthClient.client("test-app", "test-secret");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingAssertionParameter() {
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(null).send();
|
||||
assertFailure("Missing parameter:" + OAuth2Constants.ASSERTION, response, events.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadAssertionParameter() {
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest("fake-jwt").send();
|
||||
assertFailure("The provided assertion is not a valid JWT", response, events.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExpiredAssertion() {
|
||||
String jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, null));
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Token exp claim is required", response, events.poll());
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() - 1L));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Token is not active", response, events.poll());
|
||||
|
||||
//test max exp default settings
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 301L));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Token expiration is too far in the future and iat claim not present in token", response, events.poll());
|
||||
|
||||
//reduce max expiration to 10 seconds
|
||||
realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> {
|
||||
rep.getConfig().put(OIDCIdentityProviderConfig.JWT_AUTHORIZATION_GRANT_MAX_ALLOWED_ASSERTION_EXPIRATION, "10");
|
||||
});
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 11L));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Token expiration is too far in the future and iat claim not present in token", response, events.poll());
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 5L));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
|
||||
//test with iat
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 20L, (long) Time.currentTime()));
|
||||
timeOffSet.set(15);
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Token was issued too far in the past to be used now", response, events.poll());
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 20L, (long) Time.currentTime()));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAudience() {
|
||||
String jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", null, IDP_ISSUER, Time.currentTime() + 300L));
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Invalid token audience", response, events.poll());
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", "fake-audience", IDP_ISSUER));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Invalid token audience", response, events.poll());
|
||||
|
||||
// Issuer as audience works
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
|
||||
// Token endpoint as audience works
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getToken(), IDP_ISSUER));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
|
||||
// Introspection endpoint as audience does not work
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIntrospection(), IDP_ISSUER));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Invalid token audience", response, events.poll());
|
||||
|
||||
// Multiple audiences does not work
|
||||
JsonWebToken jwtToken = createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER);
|
||||
jwtToken.addAudience("fake");
|
||||
jwt = getIdentityProvider().encodeToken(jwtToken);
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Multiple audiences not allowed", response, events.poll());
|
||||
|
||||
// Multiple audiences does not work (even if both are valid)
|
||||
jwtToken = createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER);
|
||||
jwtToken.addAudience(oAuthClient.getEndpoints().getToken());
|
||||
jwt = getIdentityProvider().encodeToken(jwtToken);
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Multiple audiences not allowed", response, events.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadIssuer() {
|
||||
String jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), null, Time.currentTime() + 300L));
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Missing claim: " + OAuth2Constants.ISSUER, response, events.poll());
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), "fake-issuer", Time.currentTime() + 300L));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("No Identity Provider for provided issuer", response, events.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadSubject() {
|
||||
String jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken(null, oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 300L));
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Missing claim: " + IDToken.SUBJECT, response, events.poll());
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("fake-user", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 300L));
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("User not found", response, events.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReplayToken() {
|
||||
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Token reuse detected", response, events.poll());
|
||||
|
||||
realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> {
|
||||
rep.getConfig().put(OIDCIdentityProviderConfig.JWT_AUTHORIZATION_GRANT_ASSERTION_REUSE_ALLOWED, "true");
|
||||
});
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignatureAlg() {
|
||||
realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> {
|
||||
rep.getConfig().put(OIDCIdentityProviderConfig.JWT_AUTHORIZATION_GRANT_ASSERTION_SIGNATURE_ALG, Algorithm.ES256);
|
||||
});
|
||||
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
|
||||
realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> {
|
||||
rep.getConfig().put(OIDCIdentityProviderConfig.JWT_AUTHORIZATION_GRANT_ASSERTION_SIGNATURE_ALG, Algorithm.ES512);
|
||||
});
|
||||
jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Invalid signature algorithm", response, events.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidSignature() {
|
||||
JsonWebToken token = createDefaultAuthorizationGrantToken();
|
||||
OAuthIdentityProvider.OAuthIdentityProviderKeys newKeys = getIdentityProvider().createKeys();
|
||||
OAuthIdentityProvider.OAuthIdentityProviderKeys keys = getIdentityProvider().getKeys();
|
||||
newKeys.getKeyWrapper().setKid(keys.getKeyWrapper().getKid());
|
||||
String jwt = getIdentityProvider().encodeToken(token, newKeys);
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("Invalid signature", response, events.poll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScope() {
|
||||
oAuthClient.openid(false).scope("address phone");
|
||||
try {
|
||||
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
AccessToken token = assertSuccess("test-app", "basic-user", response);
|
||||
MatcherAssert.assertThat(List.of(token.getScope().split(" ")), Matchers.containsInAnyOrder(new String[]{"email", "profile", "address", "phone"}));
|
||||
|
||||
jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
oAuthClient.scope("address phone wrong-scope");
|
||||
response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("invalid_scope", "Invalid scopes: address phone wrong-scope", response, events.poll());
|
||||
} finally {
|
||||
oAuthClient.openid(true).scope(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessGrant() {
|
||||
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertSuccess("test-app", "basic-user", response);
|
||||
}
|
||||
|
||||
protected JsonWebToken createDefaultAuthorizationGrantToken() {
|
||||
return createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 300L);
|
||||
}
|
||||
|
||||
protected JsonWebToken createAuthorizationGrantToken(String subject, String audience, String issuer) {
|
||||
return createAuthorizationGrantToken(subject, audience, issuer, Time.currentTime() + 300L, (long) Time.currentTime());
|
||||
}
|
||||
|
||||
protected JsonWebToken createAuthorizationGrantToken(String subject, String audience, String issuer, Long exp) {
|
||||
return createAuthorizationGrantToken(subject, audience, issuer, exp, null);
|
||||
}
|
||||
|
||||
protected JsonWebToken createAuthorizationGrantToken(String subject, String audience, String issuer, Long exp, Long iat) {
|
||||
JsonWebToken token = new JsonWebToken();
|
||||
token.id(UUID.randomUUID().toString());
|
||||
token.subject(subject);
|
||||
token.audience(audience);
|
||||
token.issuer(issuer);
|
||||
token.exp(exp);
|
||||
token.iat(iat);
|
||||
return token;
|
||||
}
|
||||
|
||||
public OAuthIdentityProvider getIdentityProvider() {
|
||||
return identityProvider;
|
||||
}
|
||||
|
||||
public static class AGIdpConfig implements OAuthIdentityProviderConfig {
|
||||
|
||||
@Override
|
||||
public OAuthIdentityProviderConfigBuilder configure(OAuthIdentityProviderConfigBuilder config) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
public static class JWTAuthorizationGrantServerConfig extends ClientAuthIdpServerConfig {
|
||||
|
||||
@Override
|
||||
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
|
||||
return super.configure(config).features(Profile.Feature.JWT_AUTHORIZATION_GRANT);
|
||||
}
|
||||
}
|
||||
|
||||
public static class JWTAuthorizationGranthRealmConfig implements RealmConfig {
|
||||
public static class JWTAuthorizationGrantRealmConfig extends AbstractJWTAuthorizationGrantTest.JWTAuthorizationGrantRealmConfig {
|
||||
|
||||
@Override
|
||||
public RealmConfigBuilder configure(RealmConfigBuilder realm) {
|
||||
|
||||
realm.addClient("test-public").publicClient(true);
|
||||
|
||||
realm.addClient("authorization-grant-disabled-client").publicClient(false).secret("test-secret");
|
||||
|
||||
realm.addClient("authorization-grant-not-allowed-idp-client").publicClient(false).attribute(OIDCConfigAttributes.JWT_AUTHORIZATION_GRANT_ENABLED, "true").secret("test-secret");
|
||||
|
||||
realm.identityProvider(
|
||||
IdentityProviderBuilder.create()
|
||||
.providerId(OIDCIdentityProviderFactory.PROVIDER_ID)
|
||||
.alias(IDP_ALIAS)
|
||||
.setAttribute(IdentityProviderModel.ISSUER, IDP_ISSUER)
|
||||
.setAttribute(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, Boolean.TRUE.toString())
|
||||
.setAttribute(OIDCIdentityProviderConfig.JWKS_URL, "http://127.0.0.1:8500/idp/jwks")
|
||||
.setAttribute(OIDCIdentityProviderConfig.USE_JWKS_URL, Boolean.TRUE.toString())
|
||||
.setAttribute(OIDCIdentityProviderConfig.JWT_AUTHORIZATION_GRANT_ENABLED, Boolean.TRUE.toString())
|
||||
.build());
|
||||
super.configure(realm);
|
||||
realm.identityProvider(IdentityProviderBuilder.create()
|
||||
.providerId(JWTAuthorizationGrantIdentityProviderFactory.PROVIDER_ID)
|
||||
.alias(IDP_ALIAS)
|
||||
.setAttribute(IdentityProviderModel.ISSUER, IDP_ISSUER)
|
||||
.setAttribute(OIDCIdentityProviderConfig.JWKS_URL, "http://127.0.0.1:8500/idp/jwks")
|
||||
.build());
|
||||
return realm;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FederatedUserConfiguration implements UserConfig {
|
||||
|
||||
@Override
|
||||
public UserConfigBuilder configure(UserConfigBuilder user) {
|
||||
return user.username("basic-user").password("password").email("basic@localhost").name("First", "Last").federatedLink(IDP_ALIAS, "basic-user-id", "basic-user");
|
||||
}
|
||||
}
|
||||
|
||||
protected AccessToken assertSuccess(String expectedClientId, String username, AccessTokenResponse response) {
|
||||
Assertions.assertTrue(response.isSuccess());
|
||||
Assertions.assertNull(response.getRefreshToken());
|
||||
AccessToken accessToken = oAuthClient.parseToken(response.getAccessToken(), AccessToken.class);
|
||||
Assertions.assertNull(accessToken.getSessionId());
|
||||
MatcherAssert.assertThat(accessToken.getId(), Matchers.startsWith("trrtag:"));
|
||||
Assertions.assertEquals(expectedClientId, accessToken.getIssuedFor());
|
||||
Assertions.assertEquals(username, accessToken.getPreferredUsername());
|
||||
EventAssertion.assertSuccess(events.poll())
|
||||
.type(EventType.LOGIN)
|
||||
.clientId(expectedClientId)
|
||||
.details("grant_type", OAuth2Constants.JWT_AUTHORIZATION_GRANT)
|
||||
.details("username", username);
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
protected void assertFailure(String expectedErrorDescription, AccessTokenResponse response, EventRepresentation event) {
|
||||
assertFailure("invalid_grant", expectedErrorDescription, response, event);
|
||||
}
|
||||
|
||||
protected void assertFailure(String expectedError, String expectedErrorDescription, AccessTokenResponse response, EventRepresentation event) {
|
||||
Assertions.assertFalse(response.isSuccess());
|
||||
Assertions.assertEquals(expectedError, response.getError());
|
||||
Assertions.assertEquals(expectedErrorDescription, response.getErrorDescription());
|
||||
EventAssertion.assertError(event)
|
||||
.type(EventType.LOGIN_ERROR)
|
||||
.error("invalid_request")
|
||||
.details("grant_type", OAuth2Constants.JWT_AUTHORIZATION_GRANT)
|
||||
.details("reason", expectedErrorDescription);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.keycloak.tests.oauth;
|
||||
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.RealmConfigBuilder;
|
||||
import org.keycloak.testsuite.util.IdentityProviderBuilder;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
@KeycloakIntegrationTest(config = OIDCIdentityProviderJWTAuthorizationGrantTest.JWTAuthorizationGrantServerConfig.class)
|
||||
@TestMethodOrder(MethodOrderer.MethodName.class)
|
||||
public class OIDCIdentityProviderJWTAuthorizationGrantTest extends AbstractJWTAuthorizationGrantTest {
|
||||
|
||||
@InjectRealm(config = OIDCIdentityProviderJWTAuthorizationGrantTest.JWTAuthorizationGrantRealmConfig.class)
|
||||
protected ManagedRealm realm;
|
||||
|
||||
@Test
|
||||
public void testNotAllowedIdentityProvider() {
|
||||
realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> {
|
||||
rep.getConfig().put(OIDCIdentityProviderConfig.JWT_AUTHORIZATION_GRANT_ENABLED, "false");
|
||||
});
|
||||
|
||||
String jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER));
|
||||
AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
|
||||
assertFailure("JWT Authorization Granted is not enabled for the identity provider", response, events.poll());
|
||||
}
|
||||
|
||||
public static class JWTAuthorizationGrantRealmConfig extends AbstractJWTAuthorizationGrantTest.JWTAuthorizationGrantRealmConfig {
|
||||
|
||||
@Override
|
||||
public RealmConfigBuilder configure(RealmConfigBuilder realm) {
|
||||
super.configure(realm);
|
||||
realm.identityProvider(
|
||||
IdentityProviderBuilder.create()
|
||||
.providerId(OIDCIdentityProviderFactory.PROVIDER_ID)
|
||||
.alias(IDP_ALIAS)
|
||||
.setAttribute(IdentityProviderModel.ISSUER, IDP_ISSUER)
|
||||
.setAttribute(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, Boolean.TRUE.toString())
|
||||
.setAttribute(OIDCIdentityProviderConfig.JWKS_URL, "http://127.0.0.1:8500/idp/jwks")
|
||||
.setAttribute(OIDCIdentityProviderConfig.USE_JWKS_URL, Boolean.TRUE.toString())
|
||||
.setAttribute(OIDCIdentityProviderConfig.JWT_AUTHORIZATION_GRANT_ENABLED, Boolean.TRUE.toString())
|
||||
.build());
|
||||
return realm;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user