29 | * SAML Group details return the details of a group based on login details of users
30 | */
31 | public class SamlGroupDetails extends GroupDetails {
32 |
33 | private final String name;
34 | private final Set members = new HashSet<>();
35 |
36 | public SamlGroupDetails(String name) {
37 | this.name = name;
38 | }
39 |
40 | @Override
41 | public String getName() {
42 | return name;
43 | }
44 |
45 | @Override
46 | public String getDisplayName() {
47 | return getName();
48 | }
49 |
50 | @Override
51 | public Set getMembers() {
52 | if (members.isEmpty()) {
53 | User.getAll().forEach(u -> {
54 | LastGrantedAuthoritiesProperty prop = u.getProperty(LastGrantedAuthoritiesProperty.class);
55 | if (hasGroupOnAuthorities(prop)) {
56 | members.add(u.getId());
57 | }
58 | });
59 | }
60 | return members;
61 | }
62 |
63 | private boolean hasGroupOnAuthorities(LastGrantedAuthoritiesProperty prop) {
64 | if (prop != null) {
65 | return prop.getAuthorities2().stream().anyMatch(a -> name.equals(a.getAuthority()));
66 | }
67 | return false;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/saml/SamlLogoutAction.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2016 CloudBees, Inc., James Nord
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 | package org.jenkinsci.plugins.saml;
25 |
26 | import hudson.Extension;
27 | import hudson.model.UnprotectedRootAction;
28 |
29 | /**
30 | * A page that shows a simple message when the user logs out.
31 | * This prevents a logout - login loop when using this security realm and Anonymous does not have {@code Overall.READ} permission.
32 | */
33 | @Extension
34 | public class SamlLogoutAction implements UnprotectedRootAction {
35 |
36 | /**
37 | * The URL of the action.
38 | */
39 | static final String POST_LOGOUT_URL = "samlLogout";
40 |
41 | @Override
42 | public String getDisplayName() {
43 | return "SAML Logout";
44 | }
45 |
46 | @Override
47 | public String getIconFileName() {
48 | // hide it
49 | return null;
50 | }
51 |
52 | @Override
53 | public String getUrlName() {
54 | return POST_LOGOUT_URL;
55 | }
56 | }
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/saml/SamlPluginConfig.java:
--------------------------------------------------------------------------------
1 | /* Licensed to Jenkins CI under one or more contributor license
2 | agreements. See the NOTICE file distributed with this work
3 | for additional information regarding copyright ownership.
4 | Jenkins CI licenses this file to you under the Apache License,
5 | Version 2.0 (the "License"); you may not use this file except
6 | in compliance with the License. You may obtain a copy of the
7 | License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing,
12 | software distributed under the License is distributed on an
13 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | KIND, either express or implied. See the License for the
15 | specific language governing permissions and limitations
16 | under the License. */
17 |
18 | package org.jenkinsci.plugins.saml;
19 |
20 | import org.apache.commons.lang.StringUtils;
21 | import jenkins.model.Jenkins;
22 | import static org.jenkinsci.plugins.saml.SamlSecurityRealm.CONSUMER_SERVICE_URL_PATH;
23 | import static org.jenkinsci.plugins.saml.SamlSecurityRealm.DEFAULT_USERNAME_CASE_CONVERSION;
24 |
25 | /**
26 | * contains all the Jenkins SAML Plugin settings
27 | */
28 | public class SamlPluginConfig {
29 | private final String displayNameAttributeName;
30 | private final String groupsAttributeName;
31 | private final int maximumAuthenticationLifetime;
32 | private final String emailAttributeName;
33 |
34 | private final IdpMetadataConfiguration idpMetadataConfiguration;
35 | private final String usernameCaseConversion;
36 | private final String usernameAttributeName;
37 | private final String logoutUrl;
38 | private final String binding;
39 |
40 | private final SamlEncryptionData encryptionData;
41 | private final SamlAdvancedConfiguration advancedConfiguration;
42 |
43 | public SamlPluginConfig(String displayNameAttributeName, String groupsAttributeName,
44 | int maximumAuthenticationLifetime, String emailAttributeName, IdpMetadataConfiguration idpMetadataConfiguration,
45 | String usernameCaseConversion, String usernameAttributeName, String logoutUrl, String binding,
46 | SamlEncryptionData encryptionData, SamlAdvancedConfiguration advancedConfiguration) {
47 | this.displayNameAttributeName = displayNameAttributeName;
48 | this.groupsAttributeName = groupsAttributeName;
49 | this.maximumAuthenticationLifetime = maximumAuthenticationLifetime;
50 | this.emailAttributeName = emailAttributeName;
51 | this.idpMetadataConfiguration = idpMetadataConfiguration;
52 | this.usernameCaseConversion = StringUtils.defaultIfBlank(usernameCaseConversion, DEFAULT_USERNAME_CASE_CONVERSION);
53 | this.usernameAttributeName = hudson.Util.fixEmptyAndTrim(usernameAttributeName);
54 | this.logoutUrl = logoutUrl;
55 | this.binding = binding;
56 | this.encryptionData = encryptionData;
57 | this.advancedConfiguration = advancedConfiguration;
58 | }
59 |
60 | public String getUsernameAttributeName() {
61 | return usernameAttributeName;
62 | }
63 |
64 |
65 | public String getDisplayNameAttributeName() {
66 | return displayNameAttributeName;
67 | }
68 |
69 | public String getGroupsAttributeName() {
70 | return groupsAttributeName;
71 | }
72 |
73 | public Integer getMaximumAuthenticationLifetime() {
74 | return maximumAuthenticationLifetime;
75 | }
76 |
77 | public SamlAdvancedConfiguration getAdvancedConfiguration() {
78 | return advancedConfiguration;
79 | }
80 |
81 | public Boolean getForceAuthn() {
82 | return getAdvancedConfiguration() != null ? getAdvancedConfiguration().getForceAuthn() : Boolean.FALSE;
83 | }
84 |
85 | public String getAuthnContextClassRef() {
86 | return getAdvancedConfiguration() != null ? getAdvancedConfiguration().getAuthnContextClassRef() : null;
87 | }
88 |
89 | public String getSpEntityId() {
90 | return getAdvancedConfiguration() != null ? getAdvancedConfiguration().getSpEntityId() : null;
91 | }
92 |
93 | public String getNameIdPolicyFormat() {
94 | return getAdvancedConfiguration() != null ? getAdvancedConfiguration().getNameIdPolicyFormat() : null;
95 | }
96 |
97 | public SamlEncryptionData getEncryptionData() {
98 | return encryptionData;
99 | }
100 |
101 | public String getUsernameCaseConversion() {
102 | return usernameCaseConversion;
103 | }
104 |
105 | public String getEmailAttributeName() {
106 | return emailAttributeName;
107 | }
108 |
109 | public String getLogoutUrl() {
110 | return logoutUrl;
111 | }
112 |
113 | public String getConsumerServiceUrl() {
114 | return baseUrl() + CONSUMER_SERVICE_URL_PATH;
115 | }
116 |
117 | public String baseUrl() {
118 | return Jenkins.get().getRootUrl();
119 | }
120 |
121 | public IdpMetadataConfiguration getIdpMetadataConfiguration() {
122 | return idpMetadataConfiguration;
123 | }
124 |
125 | public String getBinding() {
126 | return binding;
127 | }
128 |
129 | @Override
130 | public String toString() {
131 | return "SamlPluginConfig{" + "idpMetadataConfiguration='" + getIdpMetadataConfiguration() + '\''
132 | + ", displayNameAttributeName='" + getDisplayNameAttributeName() + '\'' + ", groupsAttributeName='"
133 | + getGroupsAttributeName() + '\'' + ", emailAttributeName='" + getEmailAttributeName() + '\''
134 | + ", usernameAttributeName='" + getUsernameAttributeName() + '\''
135 | + ", maximumAuthenticationLifetime=" + getMaximumAuthenticationLifetime()
136 | + ", usernameCaseConversion='" + getUsernameCaseConversion() + '\'' + ", logoutUrl='"
137 | + getLogoutUrl() + '\'' + ", binding='" + getBinding() + '\'' + ", encryptionData="
138 | + getEncryptionData() + ", advancedConfiguration=" + getAdvancedConfiguration() + '}';
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/saml/SamlProfileWrapper.java:
--------------------------------------------------------------------------------
1 | /* Licensed to Jenkins CI under one or more contributor license
2 | agreements. See the NOTICE file distributed with this work
3 | for additional information regarding copyright ownership.
4 | Jenkins CI licenses this file to you under the Apache License,
5 | Version 2.0 (the "License"); you may not use this file except
6 | in compliance with the License. You may obtain a copy of the
7 | License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing,
12 | software distributed under the License is distributed on an
13 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | KIND, either express or implied. See the License for the
15 | specific language governing permissions and limitations
16 | under the License. */
17 |
18 | package org.jenkinsci.plugins.saml;
19 |
20 | import java.io.IOException;
21 | import java.util.logging.Logger;
22 | import org.kohsuke.stapler.StaplerRequest2;
23 | import org.kohsuke.stapler.StaplerResponse2;
24 | import org.pac4j.core.context.CallContext;
25 | import org.pac4j.core.context.WebContext;
26 | import org.pac4j.core.context.session.SessionStore;
27 | import org.pac4j.core.exception.http.HttpAction;
28 | import org.pac4j.saml.client.SAML2Client;
29 | import org.pac4j.saml.credentials.SAML2AuthenticationCredentials;
30 | import org.pac4j.saml.credentials.SAML2Credentials;
31 | import org.pac4j.saml.exceptions.SAMLException;
32 | import org.pac4j.saml.profile.SAML2Profile;
33 | import org.springframework.security.authentication.BadCredentialsException;
34 |
35 | /**
36 | * Process to response from the IdP to obtain the SAML2Profile of the user.
37 | */
38 | public class SamlProfileWrapper extends OpenSAMLWrapper {
39 | private static final Logger LOG = Logger.getLogger(SamlProfileWrapper.class.getName());
40 |
41 |
42 | public SamlProfileWrapper(SamlPluginConfig samlPluginConfig, StaplerRequest2 request, StaplerResponse2 response) {
43 | this.request = request;
44 | this.response = response;
45 | this.samlPluginConfig = samlPluginConfig;
46 | }
47 |
48 | /**
49 | * @return the SAML2Profile of the user returned by the IdP.
50 | */
51 | @Override
52 | protected SAML2Profile process() {
53 | SAML2AuthenticationCredentials credentials;
54 | SAML2Profile saml2Profile;
55 | try (SAML2Client client = createSAML2Client()) {
56 | WebContext context = createWebContext();
57 | SessionStore sessionStore = createSessionStore();
58 | CallContext ctx = new CallContext(context, sessionStore);
59 | SAML2Credentials unvalidated = (SAML2Credentials) client.getCredentials(ctx).orElse(null);
60 | credentials = (SAML2AuthenticationCredentials) client.validateCredentials(ctx, unvalidated).orElse(null);
61 | saml2Profile = (SAML2Profile) client.getUserProfile(ctx, credentials).orElse(null);
62 | client.destroy();
63 | } catch (HttpAction|SAMLException e) {
64 | //if the SAMLResponse is not valid we send the user again to the IdP
65 | throw new BadCredentialsException(e.getMessage(), e);
66 | }
67 | if (saml2Profile == null) {
68 | String msg = "Could not find user profile for SAML credentials: " + credentials;
69 | LOG.severe(msg);
70 | throw new BadCredentialsException(msg);
71 | }
72 |
73 | LOG.finer(saml2Profile.toString());
74 | return saml2Profile;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/saml/SamlRedirectActionWrapper.java:
--------------------------------------------------------------------------------
1 | /* Licensed to Jenkins CI under one or more contributor license
2 | agreements. See the NOTICE file distributed with this work
3 | for additional information regarding copyright ownership.
4 | Jenkins CI licenses this file to you under the Apache License,
5 | Version 2.0 (the "License"); you may not use this file except
6 | in compliance with the License. You may obtain a copy of the
7 | License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing,
12 | software distributed under the License is distributed on an
13 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | KIND, either express or implied. See the License for the
15 | specific language governing permissions and limitations
16 | under the License. */
17 |
18 | package org.jenkinsci.plugins.saml;
19 |
20 | import java.io.IOException;
21 |
22 | import org.kohsuke.stapler.StaplerRequest2;
23 | import org.kohsuke.stapler.StaplerResponse2;
24 | import org.pac4j.core.context.CallContext;
25 | import org.pac4j.core.context.session.SessionStore;
26 | import org.pac4j.core.exception.http.HttpAction;
27 | import org.pac4j.core.exception.http.RedirectionAction;
28 | import org.pac4j.core.context.WebContext;
29 | import org.pac4j.saml.client.SAML2Client;
30 |
31 | /**
32 | * Process the current configuration and request to prepare a Redirection to the IdP.
33 | */
34 | public class SamlRedirectActionWrapper extends OpenSAMLWrapper {
35 |
36 | public SamlRedirectActionWrapper(SamlPluginConfig samlPluginConfig, StaplerRequest2 request, StaplerResponse2 response) {
37 | this.request = request;
38 | this.response = response;
39 | this.samlPluginConfig = samlPluginConfig;
40 | }
41 |
42 | /**
43 | * @return the redirection URL to the IdP.
44 | * @throws IllegalStateException if something goes wrong.
45 | */
46 | @Override
47 | protected RedirectionAction process() throws IllegalStateException {
48 | try (SAML2Client client = createSAML2Client()) {
49 | WebContext context = createWebContext();
50 | SessionStore sessionStore = createSessionStore();
51 | CallContext ctx = new CallContext(context, sessionStore);
52 | RedirectionAction redirection = client.getRedirectionAction(ctx).orElse(null);
53 | client.destroy();
54 | return redirection;
55 | } catch (HttpAction e) {
56 | throw new IllegalStateException(e);
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/saml/SamlSPMetadataWrapper.java:
--------------------------------------------------------------------------------
1 | /* Licensed to Jenkins CI under one or more contributor license
2 | agreements. See the NOTICE file distributed with this work
3 | for additional information regarding copyright ownership.
4 | Jenkins CI licenses this file to you under the Apache License,
5 | Version 2.0 (the "License"); you may not use this file except
6 | in compliance with the License. You may obtain a copy of the
7 | License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing,
12 | software distributed under the License is distributed on an
13 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | KIND, either express or implied. See the License for the
15 | specific language governing permissions and limitations
16 | under the License. */
17 |
18 | package org.jenkinsci.plugins.saml;
19 |
20 | import java.io.IOException;
21 |
22 | import org.kohsuke.stapler.HttpResponse;
23 | import org.kohsuke.stapler.HttpResponses;
24 | import org.kohsuke.stapler.StaplerRequest2;
25 | import org.kohsuke.stapler.StaplerResponse2;
26 | import org.pac4j.core.exception.TechnicalException;
27 | import org.pac4j.saml.client.SAML2Client;
28 |
29 | /**
30 | * build the Service Provider(SP) metadata from the configuration.
31 | */
32 | public class SamlSPMetadataWrapper extends OpenSAMLWrapper {
33 |
34 | public SamlSPMetadataWrapper(SamlPluginConfig samlPluginConfig, StaplerRequest2 request, StaplerResponse2 response) {
35 | this.request = request;
36 | this.response = response;
37 | this.samlPluginConfig = samlPluginConfig;
38 | }
39 |
40 | /**
41 | * @return the metadata of the SP.
42 | * @throws IllegalStateException if something goes wrong.
43 | */
44 | @Override
45 | protected HttpResponse process() throws IllegalStateException {
46 | String metadata = "";
47 | try (SAML2Client client = createSAML2Client()) {
48 | metadata = client.getServiceProviderMetadataResolver().getMetadata();
49 | client.destroy();
50 | } catch (TechnicalException e) {
51 | throw new IllegalStateException(e);
52 | }
53 | return HttpResponses.text(metadata);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/saml/SamlUserDetails.java:
--------------------------------------------------------------------------------
1 | /* Licensed to Jenkins CI under one or more contributor license
2 | agreements. See the NOTICE file distributed with this work
3 | for additional information regarding copyright ownership.
4 | Jenkins CI licenses this file to you under the Apache License,
5 | Version 2.0 (the "License"); you may not use this file except
6 | in compliance with the License. You may obtain a copy of the
7 | License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing,
12 | software distributed under the License is distributed on an
13 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | KIND, either express or implied. See the License for the
15 | specific language governing permissions and limitations
16 | under the License. */
17 |
18 | package org.jenkinsci.plugins.saml;
19 |
20 | import java.util.Arrays;
21 | import java.util.Collection;
22 | import java.util.Collections;
23 | import edu.umd.cs.findbugs.annotations.NonNull;
24 | import java.util.Arrays;
25 | import java.util.Collections;
26 | import org.springframework.security.core.GrantedAuthority;
27 | import org.springframework.security.core.userdetails.UserDetails;
28 |
29 | /**
30 | * @see UserDetails
31 | */
32 | public class SamlUserDetails implements UserDetails {
33 |
34 | private static final long serialVersionUID = 2L;
35 |
36 | private final String username;
37 | private final Collection authorities;
38 |
39 | public SamlUserDetails(@NonNull String username, Collection authorities) {
40 | this.username = username;
41 | this.authorities = Collections.unmodifiableCollection(authorities);
42 | }
43 |
44 | public Collection getAuthorities() {
45 | return authorities;
46 | }
47 |
48 | public String getPassword() {
49 | return null;
50 | }
51 |
52 | public String getUsername() {
53 | return username;
54 | }
55 |
56 | public boolean isAccountNonExpired() {
57 | return true;
58 | }
59 |
60 | public boolean isAccountNonLocked() {
61 | return true;
62 | }
63 |
64 | public boolean isCredentialsNonExpired() {
65 | return true;
66 | }
67 |
68 | public boolean isEnabled() {
69 | return true;
70 | }
71 |
72 | @Override
73 | public String toString() {
74 | return "SamlUserDetails{" + "username='" + getUsername() + '\'' + ", authorities=" + (getAuthorities() == null
75 | ? "null"
76 | : Arrays.asList(getAuthorities()).toString())
77 | + '\'' + ", isAccountNonExpired='" + isAccountNonExpired() + '\'' + ", isAccountNonLocked='"
78 | + isAccountNonLocked() + '\'' + ", isCredentialsNonExpired='" + isCredentialsNonExpired() + '\''
79 | + ", isEnabled='" + isEnabled() + '}';
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/saml/SamlUserDetailsService.java:
--------------------------------------------------------------------------------
1 | /* Licensed to Jenkins CI under one or more contributor license
2 | agreements. See the NOTICE file distributed with this work
3 | for additional information regarding copyright ownership.
4 | Jenkins CI licenses this file to you under the Apache License,
5 | Version 2.0 (the "License"); you may not use this file except
6 | in compliance with the License. You may obtain a copy of the
7 | License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing,
12 | software distributed under the License is distributed on an
13 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | KIND, either express or implied. See the License for the
15 | specific language governing permissions and limitations
16 | under the License. */
17 |
18 | package org.jenkinsci.plugins.saml;
19 |
20 | import java.util.ArrayList;
21 | import java.util.Collections;
22 | import java.util.List;
23 | import edu.umd.cs.findbugs.annotations.NonNull;
24 | import org.springframework.security.core.Authentication;
25 | import org.springframework.security.core.GrantedAuthority;
26 | import org.springframework.security.core.authority.SimpleGrantedAuthority;
27 | import org.springframework.security.core.userdetails.UserDetailsService;
28 | import hudson.model.User;
29 | import hudson.security.SecurityRealm;
30 | import hudson.security.UserMayOrMayNotExistException2;
31 | import jenkins.model.Jenkins;
32 | import jenkins.security.LastGrantedAuthoritiesProperty;
33 |
34 | /**
35 | * This service is responsible for restoring UserDetails object by userId
36 | *
37 | * @see UserDetailsService
38 | */
39 | public class SamlUserDetailsService implements UserDetailsService {
40 |
41 | public SamlUserDetails loadUserByUsername(@NonNull String username) {
42 |
43 | // try to obtain user details from current authentication details
44 | Authentication auth = Jenkins.getAuthentication2();
45 | if (username.compareTo(auth.getName()) == 0 && auth instanceof SamlAuthenticationToken) {
46 | return (SamlUserDetails) auth.getDetails();
47 | }
48 |
49 | // try to rebuild authentication details based on data stored in user storage
50 | User user = User.get(username, false, Collections.emptyMap());
51 | if (user == null) {
52 | // User logged in to Jenkins, but it could exist in the backend
53 | throw new UserMayOrMayNotExistException2(username);
54 | }
55 |
56 | Listauthorities = new ArrayList<>();
57 | authorities.add(SecurityRealm.AUTHENTICATED_AUTHORITY2);
58 |
59 | if (username.compareTo(user.getId()) == 0) {
60 | LastGrantedAuthoritiesProperty lastGranted = user.getProperty(LastGrantedAuthoritiesProperty.class);
61 | if (lastGranted != null) {
62 | for (GrantedAuthority a : lastGranted.getAuthorities2()) {
63 | if (a != SecurityRealm.AUTHENTICATED_AUTHORITY2) {
64 | SimpleGrantedAuthority ga = new SimpleGrantedAuthority(a.getAuthority());
65 | authorities.add(ga);
66 | }
67 | }
68 | }
69 | }
70 | return new SamlUserDetails(user.getId(), authorities);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/saml/SamlValidateIdPMetadata.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.saml;
2 |
3 | import java.io.IOException;
4 | import net.shibboleth.shared.component.ComponentInitializationException;
5 | import net.shibboleth.shared.xml.XMLParserException;
6 | import org.apache.commons.io.IOUtils;
7 | import org.opensaml.saml.metadata.resolver.impl.DOMMetadataResolver;
8 | import org.pac4j.saml.util.Configuration;
9 | import hudson.util.FormValidation;
10 |
11 | /**
12 | * validate the IdP metadata, this class is used from the configuration screen to validate the XML in the IdP Metadata textarea.
13 | */
14 | public class SamlValidateIdPMetadata extends OpenSAMLWrapper{
15 |
16 | private final String idpMetadata;
17 |
18 | public SamlValidateIdPMetadata(String idpMetadata){
19 | this.idpMetadata = idpMetadata;
20 | }
21 |
22 | /**
23 | * process the IdP Metadata and try to parse it, if so, then return that the validation is ok.
24 | * @return ok if the IdP Metadata it right, if not return a validation error.
25 | */
26 | @Override
27 | protected FormValidation process() {
28 | try (final java.io.InputStream in = IOUtils.toInputStream(idpMetadata, "UTF-8")) {
29 | final org.w3c.dom.Document inCommonMDDoc = Configuration.getParserPool().parse(in);
30 | final org.w3c.dom.Element metadataRoot = inCommonMDDoc.getDocumentElement();
31 | DOMMetadataResolver idpMetadataProvider = new DOMMetadataResolver(metadataRoot);
32 | idpMetadataProvider.setParserPool(Configuration.getParserPool());
33 | idpMetadataProvider.setFailFastInitialization(true);
34 | idpMetadataProvider.setRequireValidMetadata(true);
35 | idpMetadataProvider.setId(idpMetadataProvider.getClass().getCanonicalName());
36 | idpMetadataProvider.initialize();
37 | } catch (IOException e) {
38 | return FormValidation.error("The IdP Metadata not valid.", e);
39 | } catch (XMLParserException e) {
40 | return FormValidation.error("The IdP Metadata not valid XML.", e);
41 | } catch (ComponentInitializationException e) {
42 | return FormValidation.error("The IdP Metadata not valid content.", e);
43 | }
44 | return FormValidation.ok("Success");
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/saml/UpdateMetadataFromURLPeriodicWork.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.saml;
2 |
3 | import java.io.IOException;
4 | import java.util.concurrent.TimeUnit;
5 | import java.util.logging.Level;
6 | import java.util.logging.Logger;
7 | import org.apache.commons.lang.StringUtils;
8 | import hudson.Extension;
9 | import hudson.model.AsyncAperiodicWork;
10 | import jenkins.model.Jenkins;
11 |
12 | /**
13 | *
This periodic work update the IdP Metadata File, the periodicof the execution is defined on the SAML Plugin configuration.
14 | *
If the Preriod is set to 0 the Periodic work is mostly disabled, it will check the changes on config
15 | * every 10 minutes to see if it is enabled again, if the period change it is re-enabled again.
16 | */
17 | @Extension
18 | public class UpdateMetadataFromURLPeriodicWork extends AsyncAperiodicWork {
19 | private static final Logger LOG = Logger.getLogger(UpdateMetadataFromURLPeriodicWork.class.getName());
20 | /**
21 | * property to set the initial delay of the AsyncAperiodicWork.
22 | * -Dorg.jenkinsci.plugins.saml.UpdateMetadataFromURLPeriodicWork.initialDelay=MILLISECONDS
23 | */
24 | public static final String INITIAL_DELAY_PROPERTY = UpdateMetadataFromURLPeriodicWork.class.getName() + ".initialDelay";
25 | public static final long INITIAL_DELAY = Long.parseLong(System.getProperty(INITIAL_DELAY_PROPERTY, "10000"));
26 |
27 | /**
28 | * {@inheritDoc}
29 | */
30 | @SuppressWarnings("unused")
31 | public UpdateMetadataFromURLPeriodicWork() {
32 | super("Update IdP Metadata from URL PeriodicWork");
33 | }
34 |
35 | /**
36 | * @return the configured period, if the configured period is 0 return 10 minutes,
37 | * if we are starting the Jenkins instance schedule an execution after 10 seconds.
38 | */
39 | @Override
40 | public long getRecurrencePeriod() {
41 | long ret = getConfiguredPeriod();
42 | if (ret == 0) {
43 | ret = TimeUnit.MINUTES.toMillis(10);
44 | }
45 | return ret;
46 | }
47 |
48 | /**
49 | * {@inheritDoc}
50 | */
51 | @Override
52 | public long getInitialDelay() {
53 | return INITIAL_DELAY;
54 | }
55 |
56 | /**
57 | * @return check the configured period in the SAML Plugin configuration.
58 | */
59 | private long getConfiguredPeriod() {
60 | long ret = 0;
61 | jenkins.model.Jenkins j = jenkins.model.Jenkins.get();
62 | if (j.getSecurityRealm() instanceof SamlSecurityRealm) {
63 | SamlSecurityRealm samlSecurityRealm = (SamlSecurityRealm) j.getSecurityRealm();
64 | IdpMetadataConfiguration config = samlSecurityRealm.getIdpMetadataConfiguration();
65 | if(config != null && config.getPeriod() != null && StringUtils.isNotBlank(config.getUrl())) {
66 | ret = TimeUnit.MINUTES.toMillis(config.getPeriod());
67 | }
68 | }
69 | return ret;
70 | }
71 |
72 | /**
73 | * {@inheritDoc}
74 | */
75 | @Override
76 | public hudson.model.AperiodicWork getNewInstance() {
77 | return new UpdateMetadataFromURLPeriodicWork();
78 | }
79 |
80 | /**
81 | * {@inheritDoc}
82 | *
Connect to the URL configured on the SAML configuration to get the IdP Metadata, then download it
83 | *
if the period configured is 0 it returns directly, do nothing.
84 | */
85 | @Override
86 | protected void execute(hudson.model.TaskListener listener) {
87 | if (getConfiguredPeriod() == 0) {
88 | return;
89 | }
90 |
91 | Jenkins j = Jenkins.get();
92 | if (j.getSecurityRealm() instanceof SamlSecurityRealm) {
93 | SamlSecurityRealm samlSecurityRealm = (SamlSecurityRealm) j.getSecurityRealm();
94 | try {
95 | samlSecurityRealm.getIdpMetadataConfiguration().updateIdPMetadata();
96 | } catch (IOException | IllegalArgumentException e) {
97 | LOG.log(Level.SEVERE, e.getMessage(), e);
98 | }
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/saml/conf/Attribute.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.saml.conf;
2 |
3 | import java.util.Objects;
4 | import edu.umd.cs.findbugs.annotations.NonNull;
5 | import org.kohsuke.stapler.DataBoundConstructor;
6 | import hudson.Extension;
7 | import hudson.model.Descriptor;
8 |
9 | /**
10 | * Class to configure SAML custom attributes to grab from the SAMLResponse and put in the User Profile.
11 | *
12 | * @author Kuisathaverat
13 | */
14 | public class Attribute extends AttributeEntry {
15 |
16 | /**
17 | * Name of the attribute in the SAML Response.
18 | */
19 | private final String name;
20 | /**
21 | * Name to display as attribute's value label on the user profile.
22 | */
23 | private final String displayName;
24 |
25 | @SuppressWarnings("unused")
26 | @DataBoundConstructor
27 | public Attribute(String name, String displayName) {
28 | this.name = name;
29 | this.displayName = displayName;
30 | }
31 |
32 | public String getName() {
33 | return name;
34 | }
35 |
36 | public String getDisplayName() {
37 | return displayName;
38 | }
39 |
40 | @SuppressWarnings("unused")
41 | @Extension
42 | public static final class DescriptorImpl extends Descriptor {
43 | @NonNull
44 | @Override
45 | public String getDisplayName() {
46 | return "SAML Attribute";
47 | }
48 | }
49 |
50 | @Override
51 | public boolean equals(Object o) {
52 | if (this == o) return true;
53 | if (o == null || getClass() != o.getClass()) return false;
54 | Attribute attribute = (Attribute) o;
55 | return Objects.equals(name, attribute.name) &&
56 | Objects.equals(displayName, attribute.displayName);
57 | }
58 |
59 | @Override
60 | public int hashCode() {
61 | return Objects.hash(name, displayName);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/saml/conf/AttributeEntry.java:
--------------------------------------------------------------------------------
1 | package org.jenkinsci.plugins.saml.conf;
2 |
3 | import hudson.model.AbstractDescribableImpl;
4 |
5 | /**
6 | * Parent Class for SAML Attributes configuration settings.
7 | *
8 | * @author Kuisathaverat
9 | */
10 | public abstract class AttributeEntry extends AbstractDescribableImpl {}
11 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/saml/user/LoginDetailsProperty.java:
--------------------------------------------------------------------------------
1 | /* Licensed to Jenkins CI under one or more contributor license
2 | agreements. See the NOTICE file distributed with this work
3 | for additional information regarding copyright ownership.
4 | Jenkins CI licenses this file to you under the Apache License,
5 | Version 2.0 (the "License"); you may not use this file except
6 | in compliance with the License. You may obtain a copy of the
7 | License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing,
12 | software distributed under the License is distributed on an
13 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | KIND, either express or implied. See the License for the
15 | specific language governing permissions and limitations
16 | under the License. */
17 | package org.jenkinsci.plugins.saml.user;
18 |
19 | import hudson.Extension;
20 | import hudson.model.Descriptor.FormException;
21 | import hudson.model.User;
22 | import hudson.model.UserProperty;
23 | import hudson.model.UserPropertyDescriptor;
24 | import edu.umd.cs.findbugs.annotations.NonNull;
25 | import net.sf.json.JSONObject;
26 | import org.apache.commons.lang.time.FastDateFormat;
27 | import hudson.security.SecurityRealm;
28 | import jenkins.model.Jenkins;
29 | import org.jenkinsci.plugins.saml.SamlSecurityRealm;
30 | import org.kohsuke.stapler.DataBoundConstructor;
31 | import org.kohsuke.stapler.StaplerRequest2;
32 |
33 | import java.util.Date;
34 | import java.util.logging.Level;
35 | import java.util.logging.Logger;
36 | import net.sf.json.JSONObject;
37 | import org.apache.commons.lang.time.FastDateFormat;
38 | import org.jenkinsci.plugins.saml.SamlSecurityRealm;
39 | import org.kohsuke.stapler.DataBoundConstructor;
40 | import org.kohsuke.stapler.StaplerRequest2;
41 | import org.springframework.security.core.Authentication;
42 | import hudson.Extension;
43 | import hudson.model.User;
44 | import hudson.model.UserProperty;
45 | import hudson.model.UserPropertyDescriptor;
46 | import hudson.security.SecurityRealm;
47 | import jenkins.model.Jenkins;
48 |
49 | /**
50 | * Store details about create and login processes
51 | *
52 | * @author Kuisathaverat
53 | */
54 | public class LoginDetailsProperty extends UserProperty {
55 | private static final Logger LOG = Logger.getLogger(LoginDetailsProperty.class.getName());
56 | private static final String ISO_8601 = "yyyy-MM-dd'T'HH:mm:ssZ";
57 | private long createTimestamp;
58 | private long lastLoginTimestamp;
59 | private long loginCount;
60 |
61 |
62 | @SuppressWarnings("unused")
63 | @DataBoundConstructor
64 | public LoginDetailsProperty() {
65 | //NOOP
66 | }
67 |
68 | @SuppressWarnings("unused")
69 | public static LoginDetailsProperty currentUserLoginDetails() {
70 | User user = User.current();
71 | LoginDetailsProperty loginDetails = null;
72 | if (user != null && user.getProperty(LoginDetailsProperty.class) != null) {
73 | loginDetails = user.getProperty(LoginDetailsProperty.class);
74 | }
75 | return loginDetails;
76 | }
77 |
78 | @SuppressWarnings("unused")
79 | public static void currentUserSetLoginDetails() {
80 | User user = User.current();
81 | if (user != null && user.getProperty(LoginDetailsProperty.class) != null) {
82 | LoginDetailsProperty loginDetails = user.getProperty(LoginDetailsProperty.class);
83 | loginDetails.update();
84 | }
85 | }
86 |
87 | public void update() {
88 | long now = System.currentTimeMillis();
89 | if (getCreateTimestamp() == 0) {
90 | setCreateTimestamp(now);
91 | }
92 |
93 | setLastLoginTimestamp(now);
94 | setLoginCount(getLoginCount() + 1);
95 | try {
96 | user.save();
97 | } catch (java.io.IOException e) {
98 | LOG.log(Level.WARNING, e.getMessage(), e);
99 | }
100 | }
101 |
102 | public long getCreateTimestamp() {
103 | return createTimestamp;
104 | }
105 |
106 | @SuppressWarnings("unused")
107 | public long getLastLoginTimestamp() {
108 | return lastLoginTimestamp;
109 | }
110 |
111 | @SuppressWarnings("unused")
112 | public String getCreateDate() {
113 | return FastDateFormat.getInstance(ISO_8601).format(new Date(createTimestamp));
114 | }
115 |
116 | @SuppressWarnings("unused")
117 | public String getLastLoginDate() {
118 | return FastDateFormat.getInstance(ISO_8601).format(new Date(lastLoginTimestamp));
119 | }
120 |
121 | public long getLoginCount() {
122 | return loginCount;
123 | }
124 |
125 | public void setCreateTimestamp(long createTimestamp) {
126 | this.createTimestamp = createTimestamp;
127 | }
128 |
129 | public void setLastLoginTimestamp(long lastLoginTimestamp) {
130 | this.lastLoginTimestamp = lastLoginTimestamp;
131 | }
132 |
133 | public void setLoginCount(long loginCount) {
134 | this.loginCount = loginCount;
135 | }
136 |
137 | @Override
138 | public UserProperty reconfigure(StaplerRequest2 req, JSONObject form) {
139 | return this;
140 | }
141 |
142 |
143 | /**
144 | * Listen to the login success/failure event to persist {@link LoginDetailsProperty}s properly.
145 | */
146 | @SuppressWarnings("unused")
147 | @Extension
148 | public static class SecurityListenerImpl extends jenkins.security.SecurityListener {
149 |
150 | @Override
151 | protected void loggedIn(@edu.umd.cs.findbugs.annotations.NonNull String username) {
152 |
153 | SecurityRealm realm = Jenkins.get().getSecurityRealm();
154 | if (!(realm instanceof SamlSecurityRealm)) {
155 | return;
156 | }
157 |
158 |
159 | try {
160 | User u = User.getById(username, true);
161 | LoginDetailsProperty o = u.getProperty(LoginDetailsProperty.class);
162 | if (o == null) {
163 | o = new LoginDetailsProperty();
164 | }
165 | u.addProperty(o);
166 | Authentication a = Jenkins.getAuthentication2();
167 | if (a.getName().equals(username)) {
168 | o.update(); // just for defensive sanity checking
169 | }
170 | } catch (java.io.IOException e) {
171 | LOG.log(Level.WARNING, "Failed to record granted authorities", e);
172 | }
173 | }
174 |
175 | }
176 |
177 |
178 | @SuppressWarnings("unused")
179 | @Extension
180 | public static final class DescriptorImpl extends UserPropertyDescriptor {
181 | @NonNull
182 | @Override
183 | public String getDisplayName() {
184 | return "User Login Properties";
185 | }
186 |
187 | public LoginDetailsProperty newInstance(User user) {
188 | return new LoginDetailsProperty();
189 | }
190 |
191 | @Override
192 | public boolean isEnabled() {
193 | return Jenkins.get().getSecurityRealm() instanceof SamlSecurityRealm;
194 | }
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/saml/user/SamlCustomProperty.java:
--------------------------------------------------------------------------------
1 | /* Licensed to Jenkins CI under one or more contributor license
2 | agreements. See the NOTICE file distributed with this work
3 | for additional information regarding copyright ownership.
4 | Jenkins CI licenses this file to you under the Apache License,
5 | Version 2.0 (the "License"); you may not use this file except
6 | in compliance with the License. You may obtain a copy of the
7 | License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing,
12 | software distributed under the License is distributed on an
13 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | KIND, either express or implied. See the License for the
15 | specific language governing permissions and limitations
16 | under the License. */
17 | package org.jenkinsci.plugins.saml.user;
18 |
19 | import java.util.ArrayList;
20 | import java.util.List;
21 | import java.util.Objects;
22 | import edu.umd.cs.findbugs.annotations.NonNull;
23 | import net.sf.json.JSONObject;
24 | import org.jenkinsci.plugins.saml.SamlSecurityRealm;
25 | import org.kohsuke.stapler.DataBoundConstructor;
26 | import org.kohsuke.stapler.StaplerRequest2;
27 | import hudson.Extension;
28 | import hudson.model.AbstractDescribableImpl;
29 | import hudson.model.Descriptor;
30 | import hudson.model.User;
31 | import hudson.model.UserProperty;
32 | import hudson.model.UserPropertyDescriptor;
33 | import jenkins.model.Jenkins;
34 |
35 | /**
36 | * Store custom SAMl Attributes read from SAML Response.
37 | *
38 | * @author Kuisathaverat
39 | */
40 | public class SamlCustomProperty extends UserProperty {
41 | /**
42 | * list of custom Attributes.
43 | */
44 | List attributes;
45 |
46 | public static class Attribute extends AbstractDescribableImpl {
47 |
48 | /**
49 | * Name of the attribute in the SAML Response.
50 | */
51 | private final String name;
52 | /**
53 | * Name to display as attribute's value label on the user profile.
54 | */
55 | private final String displayName;
56 | /**
57 | * value of the attribute.
58 | */
59 | private String value;
60 |
61 | public Attribute(String name, String displayName) {
62 | this.name = name;
63 | this.displayName = displayName;
64 | }
65 |
66 | @SuppressWarnings("unused")
67 | public String getName() {
68 | return name;
69 | }
70 |
71 | @SuppressWarnings("unused")
72 | public String getDisplayName() {
73 | return displayName;
74 | }
75 |
76 | @SuppressWarnings("unused")
77 | public String getValue() {
78 | return value;
79 | }
80 |
81 | public void setValue(String value) {
82 | this.value = value;
83 | }
84 |
85 | @Override
86 | public boolean equals(Object o) {
87 | if (this == o) return true;
88 | if (o == null || getClass() != o.getClass()) return false;
89 | Attribute attribute = (Attribute) o;
90 | return Objects.equals(name, attribute.name) &&
91 | Objects.equals(displayName, attribute.displayName) &&
92 | Objects.equals(value, attribute.value);
93 | }
94 |
95 | @Override
96 | public int hashCode() {
97 |
98 | return Objects.hash(name, displayName, value);
99 | }
100 |
101 | @SuppressWarnings("unused")
102 | @Extension
103 | public static final class DescriptorImpl extends Descriptor {
104 | @NonNull
105 | @Override
106 | public String getDisplayName() {
107 | return "SAML Attribute";
108 | }
109 | }
110 |
111 | }
112 |
113 | @DataBoundConstructor
114 | public SamlCustomProperty(List attributes) {
115 | this.attributes = attributes;
116 | }
117 |
118 | @NonNull
119 | public List getAttributes(){
120 | if(attributes == null){
121 | return java.util.Collections.emptyList();
122 | }
123 | return attributes;
124 | }
125 |
126 | @SuppressWarnings("unused")
127 | public void setAttributes(List attributes) {
128 | this.attributes = attributes;
129 | }
130 |
131 | @Override
132 | public UserProperty reconfigure(StaplerRequest2 req, JSONObject form) {
133 | return this;
134 | }
135 |
136 | @SuppressWarnings("unused")
137 | @Extension
138 | public static final class DescriptorImpl extends UserPropertyDescriptor {
139 | @NonNull
140 | public String getDisplayName() {
141 | return "Saml Custom Attributes property";
142 | }
143 |
144 | public SamlCustomProperty newInstance(User user) {
145 | return new SamlCustomProperty(new ArrayList<>());
146 | }
147 |
148 | @Override
149 | public boolean isEnabled() {
150 | return Jenkins.get().getSecurityRealm() instanceof SamlSecurityRealm;
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/main/resources/index.jelly:
--------------------------------------------------------------------------------
1 |
2 |
3 | This plugin enables use of a SAML 2.0 authentication source for single sign-on support.
4 |
2 | If this field is not empty, request that the SAML IdP uses a specific
3 | authentication context, rather than its default. Check with the IdP
4 | administrators to find out which authentication contexts are available.
5 |
2 | Whether to request the SAML IdP to force (re)authentication of the user, rather than allowing an existing session with the IdP to be reused. Off by default.
3 |
2 | When you enable this option the value of the relayState parameter sent to the IdP is a random generated value.
3 | The default value of relayState is JENKINS_URL/securityRealm/finishLogin
4 |
2 | If this field is not empty, it overrides the default Entity ID for this Service Provider.
3 | Service Provider Entity IDs are usually a URL, like http://jenkins.example.org/.
4 |
2 | The SP metadata is written on every login,
3 | enable this setting change the behaviour to use cache,
4 | and save the file only if it has changes.
5 |
2 | Enable signature of the Auth Request. If you enable it the encryption and signing key would available in the SP metadata file and URL (JENKINS_URL/securityRealm/metadata).
3 | Disable signing auth request does not work with HTTP redirection binging, it only works for POST binding.
4 |
2 | It requests signed assertions send by the IdP.
3 | The WantAssertionsSigned attribute on the <md:SPSSODescriptor> element declares
4 | that the service provider wants the <saml:Assertion> element to be digitally signed
5 | This attribute causes a metadata-aware identity provider to auto-configure itself at run time.
6 |
2 | You could enable this options to use SAML ForceAuthn to force logins at our IdP,
3 | AuthnContextClassRef to override the default authentication mechanism,
4 | and force multi-factor authentication;
5 | you also could set the sessions on Jenkins to be shorter than those on your IdP.
6 |
2 | SAML Plugin supports two method of redirection binding HTTP-Redirect and HTTP-POST, by default HTTP-Redirect is used.
3 | Check supported binding redirection types of your IdP.
4 |
2 | The SAML standard allows using a key-pair to authenticate and encrypt messages between
3 | service providers and identity providers.The IdP Metadata entered above contains the
4 | IdP's public key, and in order to use encryption for the messages passed from IdP to SP,
5 | you need to generate a key and enter the details here. Your IdP may or may not require
6 | or implement this encryption - check with the IdP administrator if unsure.
7 |
8 | The key can be created using the following command:
9 |
15 | where pw1 and pw2 are the key and store passwords respectively. These passwords need to be entered in the corresponding fields
16 | below. The validity period given above is 10 years, feel free to choose whatever period suits you.
17 |
2 | Number of seconds since user was authenticated in IdP while his authentication is considering as active.
3 | If you often get "No valid subject assertion found in response" or "Authentication issue instant is too old or in the future"
4 | then most probably you need to increase this value.
5 |
6 | Default is 24h * 60 min * 60 sec = 86400
7 |
2 | The ID returned from SAML is used as the username for Authorization, which
3 | is usually case-sensitive. To make it easier to match with user definition
4 | in the policy, the returned value can be converted.
5 |
6 |
None - will not change return value
7 |
Lowercase - convert to lowercase
8 |
Uppercase - convert to uppercase
9 |
10 | Caution! Be aware of case in Authorization strategy as you
11 | may lose access rights if they do no match.
12 |