Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some smaller housekeeping #509

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
4 changes: 2 additions & 2 deletions docs/configuration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ which will also help discovering your settings

From 1.5 and onward the well known configuration location may be used to
populate the configuration simplifying the configuration greatly.
The switch between modes is controled by the `serverConfiguration` field
The switch between modes is controlled by the `serverConfiguration` field

| field | format | description |
|----------------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------|
Expand Down Expand Up @@ -154,7 +154,7 @@ jenkins:
tokenExpirationCheckDisabled: <boolean>
# escape hatch
escapeHatchEnabled: <boolean>
escapeHatchUsername: escapeHatchUsername
escapeHatchUsername: <string>
escapeHatchSecret: <string:secret>
escapeHatchGroup: <string>
```
55 changes: 20 additions & 35 deletions src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java
Original file line number Diff line number Diff line change
Expand Up @@ -183,23 +183,23 @@
@Deprecated
private transient String wellKnownOpenIDConfigurationUrl;

/** @deprecated see {@link OicServerConfiguration#getTokenServerUrl()} */
/** @deprecated see {@link OicServerManualConfiguration#getTokenServerUrl()} */
@Deprecated
private transient String tokenServerUrl;

/** @deprecated see {@link OicServerConfiguration#getJwksServerUrl()} */
/** @deprecated see {@link OicServerManualConfiguration#getJwksServerUrl()} */
@Deprecated
private transient String jwksServerUrl;

/** @deprecated see {@link OicServerConfiguration#getTokenAuthMethod()} */
/** @deprecated see {@link OicServerManualConfiguration#getTokenAuthMethod()} */
@Deprecated
private transient TokenAuthMethod tokenAuthMethod;

/** @deprecated see {@link OicServerConfiguration#getAuthorizationServerUrl()} */
/** @deprecated see {@link OicServerManualConfiguration#getAuthorizationServerUrl()} */
@Deprecated
private transient String authorizationServerUrl;

/** @deprecated see {@link OicServerConfiguration#getUserInfoServerUrl()} */
/** @deprecated see {@link OicServerManualConfiguration#getUserInfoServerUrl()} */
@Deprecated
private transient String userInfoServerUrl;

Expand All @@ -218,14 +218,14 @@
private transient String simpleGroupsFieldName = null;
private transient String nestedGroupFieldName = null;

/** @deprecated see {@link OicServerConfiguration#getScopes()} */
/** @deprecated see {@link OicServerManualConfiguration#getScopes()} */
@Deprecated
private transient String scopes = null;

private final boolean disableSslVerification;
private boolean logoutFromOpenidProvider = true;

/** @deprecated see {@link OicServerConfiguration#getEndSessionUrl()} */
/** @deprecated see {@link OicServerManualConfiguration#getEndSessionUrl()} */
@Deprecated
private transient String endSessionEndpoint = null;

Expand Down Expand Up @@ -294,7 +294,7 @@
SystemProperties.getBoolean(OicSecurityRealm.class.getName() + ".checkNonceInRefreshFlow", false);

/** old field that had an '/' implicitly added at the end,
* transient because we no longer want to have this value stored
* transient because we no longer want to have this value stored,
* but it's still needed for backwards compatibility */
@Deprecated
private transient String endSessionUrl;
Expand Down Expand Up @@ -627,12 +627,6 @@
oidcProviderMetadata.setUserInfoJWEEncs(userInfoJWEEncs);
}

if (oidcProviderMetadata.getRequestObjectJWEEncs() != null) {
List<EncryptionMethod> requestObjectJweEncs = OicAlgorithmValidatorFIPS140.getFipsCompliantEncryptionMethod(
oidcProviderMetadata.getRequestObjectJWEEncs());
oidcProviderMetadata.setRequestObjectJWEEncs(requestObjectJweEncs);
}

if (oidcProviderMetadata.getAuthorizationJWEEncs() != null) {
List<EncryptionMethod> authJweEncs = OicAlgorithmValidatorFIPS140.getFipsCompliantEncryptionMethod(
oidcProviderMetadata.getAuthorizationJWEEncs());
Expand Down Expand Up @@ -723,9 +717,9 @@
}

if (oidcProviderMetadata.getClientRegistrationAuthnJWSAlgs() != null) {
List<JWSAlgorithm> clientRegisterationAuth = OicAlgorithmValidatorFIPS140.getFipsCompliantJWSAlgorithm(
List<JWSAlgorithm> clientRegistrationAuth = OicAlgorithmValidatorFIPS140.getFipsCompliantJWSAlgorithm(

Check warning on line 720 in src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java#L720

Added line #L720 was not covered by tests
oidcProviderMetadata.getClientRegistrationAuthnJWSAlgs());
oidcProviderMetadata.setClientRegistrationAuthnJWSAlgs(clientRegisterationAuth);
oidcProviderMetadata.setClientRegistrationAuthnJWSAlgs(clientRegistrationAuth);

Check warning on line 722 in src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 720-722 are not covered by tests

Check warning on line 722 in src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java#L722

Added line #L722 was not covered by tests
}
}

Expand Down Expand Up @@ -791,14 +785,10 @@
return null;
}

private Object applyJMESPath(Expression<Object> expression, Object map) {
return expression.search(map);
}

@DataBoundSetter
public void setGroupsFieldName(String groupsFieldName) {
this.groupsFieldName = Util.fixEmptyAndTrim(groupsFieldName);
this.groupsFieldExpr = this.compileJMESPath(this.groupsFieldName, "groups field");
this.groupsFieldExpr = compileJMESPath(this.groupsFieldName, "groups field");
}

@DataBoundSetter
Expand Down Expand Up @@ -962,7 +952,7 @@
* Validate post-login redirect URL
*
* For security reasons, the login must not redirect outside Jenkins
* realm. For useablility reason, the logout page should redirect to
* realm. For usability reason, the logout page should redirect to
* root url.
*/
protected String getValidRedirectUrl(String url) {
Expand All @@ -987,7 +977,7 @@
}

/**
* Handles the the securityRealm/commenceLogin resource and sends the user off to the IdP
* Handles the securityRealm/commenceLogin resource and sends the user off to the IdP
* @param from the relative URL to the page that the user has just come from
* @param referer the HTTP referer header (where to redirect the user back to after login has finished)
* @throws URISyntaxException if the provided data is invalid
Expand All @@ -1011,7 +1001,6 @@
// store the redirect url for after the login.
sessionStore.set(webContext, SESSION_POST_LOGIN_REDIRECT_URL_KEY, redirectOnFinish);
JEEHttpActionAdapter.INSTANCE.adapt(redirectionAction, webContext);
return;
}

@SuppressFBWarnings(
Expand Down Expand Up @@ -1115,11 +1104,8 @@
}
}
if (idToken != null) {
String fieldValue = Util.fixEmptyAndTrim(
return Util.fixEmptyAndTrim(
getStringField(idToken.getJWTClaimsSet().getClaims(), fieldExpr));
if (fieldValue != null) {
return fieldValue;
}
}
}
return null;
Expand All @@ -1141,7 +1127,7 @@
grantedAuthorities.add(SecurityRealm.AUTHENTICATED_AUTHORITY2);
if (this.groupsFieldExpr == null) {
if (this.groupsFieldName == null) {
LOGGER.fine("Not adding groups because groupsFieldName is not set. groupsFieldName=" + groupsFieldName);
LOGGER.fine("Not adding groups because groupsFieldName is not set.");
} else {
LOGGER.fine("Not adding groups because groupsFieldName is invalid. groupsFieldName=" + groupsFieldName);
}
Expand Down Expand Up @@ -1184,7 +1170,7 @@
LOGGER.warning("userInfo did not contain a valid group field content, got null");
return Collections.<String>emptyList();
} else if (field instanceof String) {
// if its a String, the original value was not a json array.
// if it's a String, the original value was not a json array.
// We try to convert the string to list based on comma while ignoring whitespaces and square brackets.
// Example value "[demo-user-group, demo-test-group, demo-admin-group]"
String sField = (String) field;
Expand All @@ -1203,331 +1189,330 @@
if (group instanceof String) {
result.add(group.toString());
} else if (group instanceof Map) {
// if its a Map, we use the nestedGroupFieldName to grab the groups
// if it's a Map, we use the nestedGroupFieldName to grab the groups
Map<String, String> groupMap = (Map<String, String>) group;
if (nestedGroupFieldName != null && groupMap.keySet().contains(nestedGroupFieldName)) {
if (nestedGroupFieldName != null && groupMap.containsKey(nestedGroupFieldName)) {
result.add(groupMap.get(nestedGroupFieldName));
}
}
}
return result;
} else {
try {
return (List<String>) field;
} catch (ClassCastException e) {
LOGGER.warning("userInfo did not contain a valid group field content, got: "
+ field.getClass().getSimpleName());
return Collections.<String>emptyList();
}
}
}

@Restricted(DoNotUse.class) // stapler only
public void doLogout(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = User.get2(authentication);

Assert.notNull(user, "User must not be null");

OicCredentials credentials = user.getProperty(OicCredentials.class);

if (credentials != null) {
if (this.logoutFromOpenidProvider
&& serverConfiguration.toProviderMetadata().getEndSessionEndpointURI() != null) {
// This ensures that token will be expired at the right time with API Key calls, but no refresh can be
// made.
user.addProperty(new OicCredentials(null, null, null, CLOCK.millis()));
}

req.setAttribute(ID_TOKEN_REQUEST_ATTRIBUTE, credentials.getIdToken());
}

super.doLogout(req, rsp);
}

@Override
public String getPostLogOutUrl2(StaplerRequest2 req, Authentication auth) {
Object idToken = req.getAttribute(ID_TOKEN_REQUEST_ATTRIBUTE);
Object state = getStateAttribute(req.getSession());
var openidLogoutEndpoint = maybeOpenIdLogoutEndpoint(
Objects.toString(idToken, ""), Objects.toString(state), this.postLogoutRedirectUrl);
if (openidLogoutEndpoint != null) {
return openidLogoutEndpoint;
}
return getFinalLogoutUrl(req, auth);
}

@VisibleForTesting
Object getStateAttribute(HttpSession session) {
// return null;
OidcClient client = buildOidcClient();
FrameworkParameters parameters =
new JEEFrameworkParameters(Stapler.getCurrentRequest2(), Stapler.getCurrentResponse2());
WebContext webContext = JEEContextFactory.INSTANCE.newContext(parameters);
SessionStore sessionStore = JEESessionStoreFactory.INSTANCE.newSessionStore(parameters);
CallContext ctx = new CallContext(webContext, sessionStore);
return client.getConfiguration()
.getValueRetriever()
.retrieve(ctx, client.getStateSessionAttributeName(), client)
.orElse(null);
}

@CheckForNull
private String maybeOpenIdLogoutEndpoint(String idToken, String state, String postLogoutRedirectUrl) {
final URI url = serverConfiguration.toProviderMetadata().getEndSessionEndpointURI();
if (this.logoutFromOpenidProvider && url != null) {
StringBuilder openidLogoutEndpoint = new StringBuilder(url.toString());

if (!Strings.isNullOrEmpty(idToken)) {
openidLogoutEndpoint.append("?id_token_hint=").append(idToken).append("&");
} else {
openidLogoutEndpoint.append("?");
}
openidLogoutEndpoint.append("state=").append(state);

if (postLogoutRedirectUrl != null) {
openidLogoutEndpoint
.append("&post_logout_redirect_uri=")
.append(URLEncoder.encode(postLogoutRedirectUrl, StandardCharsets.UTF_8));
}
return openidLogoutEndpoint.toString();
}
return null;
}

private String getFinalLogoutUrl(StaplerRequest2 req, Authentication auth) {
if (Jenkins.get().hasPermission(Jenkins.READ)) {
return super.getPostLogOutUrl2(req, auth);
}
return req.getContextPath() + "/" + OicLogoutAction.POST_LOGOUT_URL;
}

private String getRootUrl() {
if (rootURLFromRequest) {
return Jenkins.get().getRootUrlFromRequest();
} else {
return Jenkins.get().getRootUrl();
}
}

private String ensureRootUrl() {
String rootUrl = getRootUrl();
if (rootUrl == null) {
throw new NullPointerException("Jenkins root url must not be null");
} else {
return rootUrl;
}
}

private String buildOauthCommenceLogin() {
return ensureRootUrl() + getLoginUrl();
}

private String buildOAuthRedirectUrl() throws NullPointerException {
return ensureRootUrl() + "securityRealm/finishLogin";
}

/**
* This is where the user comes back to at the end of the OpenID redirect ping-pong.
* @param request The user's request
* @throws ParseException if the JWT (or other response) could not be parsed.
*/
public void doFinishLogin(StaplerRequest2 request, StaplerResponse2 response) throws IOException, ParseException {
OidcClient client = buildOidcClient();

FrameworkParameters parameters = new JEEFrameworkParameters(request, response);
WebContext webContext = JEEContextFactory.INSTANCE.newContext(parameters);
SessionStore sessionStore = JEESessionStoreFactory.INSTANCE.newSessionStore(parameters);

try {
// NB: TODO this also handles back channel logout if "logoutendpoint" parameter is set
// see org.pac4j.oidc.credentials.extractor.OidcExtractor.extract(WebContext, SessionStore)
// but we probably need to hookup a special LogoutHandler in the clients configuration to do all the special
// Jenkins stuff correctly
// also should have its own URL to make the code easier to follow :)

if (!sessionStore.renewSession(webContext)) {
throw new TechnicalException("Could not create a new session");
}

CallContext ctx = new CallContext(webContext, sessionStore);
Credentials credentials = client.getCredentials(ctx)
.orElseThrow(() -> new Failure("Could not extract credentials from request"));
credentials = client.validateCredentials(ctx, credentials)
.orElseThrow(() -> new Failure("Could not validate credentials from request"));

ProfileCreator profileCreator = client.getProfileCreator();

// creating the profile performs validation of the token
OidcProfile profile = (OidcProfile) profileCreator
.create(ctx, credentials)
.orElseThrow(() -> new Failure("Could not build user profile"));

AccessToken accessToken = profile.getAccessToken();
JWT idToken = profile.getIdToken();
RefreshToken refreshToken = profile.getRefreshToken();

String username = determineStringField(userNameFieldExpr, idToken, profile.getAttributes());
if (failedCheckOfTokenField(idToken)) {
throw new FailedCheckOfTokenException(client.getConfiguration().findLogoutUrl());
}

OicCredentials oicCredentials = new OicCredentials(
accessToken == null ? null : accessToken.getValue(), // XXX (how) can the access token be null?
idToken.getParsedString(),
refreshToken != null ? refreshToken.getValue() : null,
accessToken == null ? 0 : accessToken.getLifetime(),
CLOCK.millis(),
getAllowedTokenExpirationClockSkewSeconds());

loginAndSetUserData(username, idToken, profile.getAttributes(), oicCredentials);

String redirectUrl = (String) sessionStore
.get(webContext, SESSION_POST_LOGIN_REDIRECT_URL_KEY)
.orElse(Jenkins.get().getRootUrl());
response.sendRedirect(HttpURLConnection.HTTP_MOVED_TEMP, redirectUrl);

} catch (HttpAction e) {
// this may be an OK flow for logout login is handled upstream.
JEEHttpActionAdapter.INSTANCE.adapt(e, webContext);
return;
}
}

/**
* Handles Token Expiration.
* @throws IOException a low level exception
*/
public boolean handleTokenExpiration(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (httpRequest.getRequestURI().endsWith("/logout")) {
// No need to refresh token when logging out
return true;
}

if (authentication == null || authentication instanceof AnonymousAuthenticationToken) {
return true;
}

User user = User.get2(authentication);

if (isAllowTokenAccessWithoutOicSession()) {
// check if this is a valid api token based request
String authHeader = httpRequest.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Basic ")) {
String token = new String(Base64.getDecoder().decode(authHeader.substring(6)), StandardCharsets.UTF_8)
.split(":")[1];

if (user.getProperty(ApiTokenProperty.class).matchesPassword(token)) {
// this was a valid jenkins token being used, exit this filter and let
// the rest of chain be processed
return true;
} // else do nothing and continue evaluating this request
}
}

if (user == null) {
return true;
}

OicCredentials credentials = user.getProperty(OicCredentials.class);

if (credentials == null) {
return true;
}

if (isExpired(credentials)) {
if (serverConfiguration.toProviderMetadata().getGrantTypes() != null
&& serverConfiguration.toProviderMetadata().getGrantTypes().contains(GrantType.REFRESH_TOKEN)
&& !Strings.isNullOrEmpty(credentials.getRefreshToken())) {
LOGGER.log(Level.FINEST, "Attempting to refresh credential for user: {0}", user.getId());
boolean retVal = refreshExpiredToken(user.getId(), credentials, httpRequest, httpResponse);
LOGGER.log(Level.FINEST, "Refresh credential for user returned {0}", retVal);
return retVal;
} else if (!isTokenExpirationCheckDisabled()) {
redirectToLoginUrl(httpRequest, httpResponse);
return false;
}
}

return true;
}

private void redirectToLoginUrl(HttpServletRequest req, HttpServletResponse res) throws IOException {
if (req != null && (req.getSession(false) != null || Strings.isNullOrEmpty(req.getHeader("Authorization")))) {
req.getSession().invalidate();
}
if (res != null) {
res.sendRedirect(Jenkins.get().getSecurityRealm().getLoginUrl());
}
}

public boolean isExpired(OicCredentials credentials) {
if (credentials.getExpiresAtMillis() == null) {
return false;
}

return CLOCK.millis() >= credentials.getExpiresAtMillis();
}

private boolean refreshExpiredToken(
String expectedUsername,
OicCredentials credentials,
HttpServletRequest httpRequest,
HttpServletResponse httpResponse)
throws IOException {

FrameworkParameters parameters = new JEEFrameworkParameters(httpRequest, httpResponse);
WebContext webContext = JEEContextFactory.INSTANCE.newContext(parameters);
SessionStore sessionStore = JEESessionStoreFactory.INSTANCE.newSessionStore(parameters);
OidcClient client = buildOidcClient();
// PAC4J maintains the nonce even though servers should not respond with an id token containing the nonce
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse
// it SHOULD NOT have a nonce Claim, even when the ID Token issued at the time of the original authentication
// contained nonce;
// however, if it is present, its value MUST be the same as in the ID Token issued at the time of the original
// authentication
// by default we will strip out the nonce unless the user has opted into it.
client.getConfiguration().setUseNonce(!nonceDisabled && checkNonceInRefreshFlow);
try {
OidcProfile profile = new OidcProfile();
profile.setAccessToken(new BearerAccessToken(credentials.getAccessToken()));
profile.setIdTokenString(credentials.getIdToken());
profile.setRefreshToken(new RefreshToken(credentials.getRefreshToken()));

CallContext ctx = new CallContext(webContext, sessionStore);
profile = (OidcProfile) client.renewUserProfile(ctx, profile)
.orElseThrow(() -> new IllegalStateException("Could not renew user profile"));

// During refresh the IDToken may or may not be present.
// The refresh token may also not be present.
// in these cases we will reuse the original values.

AccessToken accessToken = profile.getAccessToken();
JWT idToken = Objects.requireNonNullElse(profile.getIdToken(), JWTParser.parse(credentials.getIdToken()));
RefreshToken refreshToken = Objects.requireNonNullElse(
profile.getRefreshToken(), new RefreshToken(credentials.getRefreshToken()));

String username = determineStringField(userNameFieldExpr, idToken, profile.getAttributes());
if (!User.idStrategy().equals(expectedUsername, username)) {
httpResponse.sendError(
HttpServletResponse.SC_UNAUTHORIZED, "User name was not the same after refresh request");
return false;
}
// the username may have changed case during a call, but still be the same user (as we have checked the
// idStrategy)
// we need to keep using exactly the same principal otherwise there is a potential for crumbs not to match.
// whilst we could do some normalization of the username, just use the original (expected) username
// see https://github.com/jenkinsci/oic-auth-plugin/issues/411
if (LOGGER.isLoggable(Level.FINE)) {
Authentication a = SecurityContextHolder.getContext().getAuthentication();
User u = User.get2(a);
LOGGER.log(
Level.FINE,
"Token refresh. Current Authentitcation principal: " + a.getName() + " user id:"
+ (u == null ? "null user" : u.getId()) + " newly retreived username would have been: "
"Token refresh. Current Authentication principal: " + a.getName() + " user id:"

Check warning on line 1514 in src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java#L1514

Added line #L1514 was not covered by tests
+ (u == null ? "null user" : u.getId()) + " newly retrieved username would have been: "

Check warning on line 1515 in src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 1194-1515 are not covered by tests
+ username);
}
username = expectedUsername;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,17 +153,16 @@ public OIDCProviderMetadata toProviderMetadata() {
// should really be a UI option, but was not previously
// server is mandated to support HS256 but if we do not specify things that it produces
// then they would never be checked.
// rather we just say "I support anything, and let the check for the specific ones fail and fall through
ArrayList<JWSAlgorithm> allAlgorthms = new ArrayList<>();
allAlgorthms.addAll(JWSAlgorithm.Family.HMAC_SHA);
// rather we just say "I support anything, and let the check for the specific ones fail and fall through"
ArrayList<JWSAlgorithm> allAlgorithms = new ArrayList<>(JWSAlgorithm.Family.HMAC_SHA);
if (FIPS140.useCompliantAlgorithms()) {
// In FIPS-140 Family.ED is not supported
allAlgorthms.addAll(JWSAlgorithm.Family.RSA);
allAlgorthms.addAll(JWSAlgorithm.Family.EC);
allAlgorithms.addAll(JWSAlgorithm.Family.RSA);
allAlgorithms.addAll(JWSAlgorithm.Family.EC);
} else {
allAlgorthms.addAll(JWSAlgorithm.Family.SIGNATURE);
allAlgorithms.addAll(JWSAlgorithm.Family.SIGNATURE);
}
providerMetadata.setIDTokenJWSAlgs(allAlgorthms);
providerMetadata.setIDTokenJWSAlgs(allAlgorithms);
return providerMetadata;
} catch (URISyntaxException e) {
throw new IllegalStateException("could not create provider metadata", e);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
package org.jenkinsci.plugins.oic;

import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import hudson.Util;
import hudson.model.Descriptor;
import hudson.util.FormValidation;
import java.net.URISyntaxException;
import jenkins.security.FIPS140;
import org.hamcrest.Matcher;
import org.jenkinsci.plugins.oic.OicServerManualConfiguration.DescriptorImpl;
import org.junit.jupiter.api.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.WithoutJenkins;
import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
import org.mockito.MockedStatic;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.jvnet.hudson.test.JenkinsMatchers.hasKind;
import static org.mockito.Mockito.mockStatic;

@WithJenkins
class OicServerManualConfigurationTest {
Expand Down Expand Up @@ -88,6 +99,31 @@ void doCheckEndSessionEndpoint(JenkinsRule jenkinsRule) {
assertThat(descriptor.doCheckEndSessionUrl("http://localhost.jwks"), hasKind(FormValidation.Kind.OK));
}

@Test
@WithoutJenkins
public void testProviderMetadataWithFips() throws Descriptor.FormException {
OicServerManualConfiguration config = new OicServerManualConfiguration("issuer", "t-url", "a-url");
try (MockedStatic<FIPS140> fips140Mock = mockStatic(FIPS140.class)) {
JWSAlgorithm.Family ed = JWSAlgorithm.Family.ED;
JWSAlgorithm arbitraryEdAlgorithm = (JWSAlgorithm) ed.toArray()[0];

fips140Mock.when(FIPS140::useCompliantAlgorithms).thenReturn(true);
OIDCProviderMetadata data = config.toProviderMetadata();
assertFalse(data.getIDTokenJWSAlgs().contains(arbitraryEdAlgorithm));

fips140Mock.when(FIPS140::useCompliantAlgorithms).thenReturn(false);
data = config.toProviderMetadata();
assertTrue(data.getIDTokenJWSAlgs().contains(arbitraryEdAlgorithm));
}
}

@Test
@WithoutJenkins
public void testProviderMetadataWithInvalidURI() throws Descriptor.FormException, URISyntaxException {
OicServerManualConfiguration config = new OicServerManualConfiguration("issuer", "t-url", "inv%alid");
assertThrows(IllegalStateException.class, config::toProviderMetadata);
}

private static DescriptorImpl getDescriptor(JenkinsRule jenkinsRule) {
return (DescriptorImpl) jenkinsRule.jenkins.getDescriptor(OicServerManualConfiguration.class);
}
Expand Down
Loading