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

Allow to add nickname as attribute during a registration request #789

Merged
merged 7 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ public static class RegistrationProperties {
boolean showRegistrationButtonInLoginPage = true;

boolean requireExternalAuthentication = false;

boolean addNicknameAsAttribute = false;

ExternalAuthenticationType authenticationType;

Expand All @@ -245,6 +247,14 @@ public boolean isRequireExternalAuthentication() {
public void setRequireExternalAuthentication(boolean requireExternalAuthentication) {
this.requireExternalAuthentication = requireExternalAuthentication;
}

public boolean isAddNicknameAsAttribute() {
return addNicknameAsAttribute;
}

public void setAddNicknameAsAttribute(boolean addNicknameAsAttribute) {
this.addNicknameAsAttribute = addNicknameAsAttribute;
}

public ExternalAuthenticationType getAuthenticationType() {
return authenticationType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@
import it.infn.mw.iam.audit.events.registration.RegistrationRequestEvent;
import it.infn.mw.iam.authn.ExternalAuthenticationRegistrationInfo;
import it.infn.mw.iam.authn.ExternalAuthenticationRegistrationInfo.ExternalAuthenticationType;
import it.infn.mw.iam.config.IamProperties;
import it.infn.mw.iam.config.lifecycle.LifecycleProperties;
import it.infn.mw.iam.core.IamRegistrationRequestStatus;
import it.infn.mw.iam.core.user.IamAccountService;
import it.infn.mw.iam.notification.NotificationFactory;
import it.infn.mw.iam.persistence.model.IamAccount;
import it.infn.mw.iam.persistence.model.IamAttribute;
import it.infn.mw.iam.persistence.model.IamAup;
import it.infn.mw.iam.persistence.model.IamAupSignature;
import it.infn.mw.iam.persistence.model.IamLabel;
Expand Down Expand Up @@ -121,8 +123,13 @@ public class DefaultRegistrationRequestService
@Autowired
private Clock clock;

@Autowired
private IamProperties iamProperties;

private ApplicationEventPublisher eventPublisher;

public static final String NICKNAME_ATTRIBUTE_KEY = "nickname";

private IamRegistrationRequest findRequestById(String requestUuid) {
return requestRepository.findByUuid(requestUuid)
.orElseThrow(() -> new ScimResourceNotFoundException(
Expand Down Expand Up @@ -165,7 +172,8 @@ private void addExternalAuthnInfo(ScimUser.Builder user,
private void createAupSignatureForAccountIfNeeded(IamAccount account) {
Optional<IamAup> aup = iamAupRepo.findDefaultAup();
if (aup.isPresent()) {
IamAupSignature signature = iamAupSignatureRepo.createSignatureForAccount(aup.get(), account, Date.from(clock.instant()));
IamAupSignature signature = iamAupSignatureRepo.createSignatureForAccount(aup.get(), account,
Date.from(clock.instant()));
eventPublisher.publishEvent(new AupSignedEvent(this, signature));
}
}
Expand Down Expand Up @@ -200,6 +208,11 @@ public RegistrationRequestDto createRequest(RegistrationRequestDto dto,
accountEntity.setConfirmationKey(tokenGenerator.generateToken());
accountEntity.setActive(false);

if (iamProperties.getRegistration().isAddNicknameAsAttribute()) {
accountEntity.getAttributes()
.add(IamAttribute.newInstance(NICKNAME_ATTRIBUTE_KEY, dto.getUsername()));
}

createAupSignatureForAccountIfNeeded(accountEntity);

IamRegistrationRequest requestEntity = new IamRegistrationRequest();
Expand Down Expand Up @@ -399,12 +412,4 @@ public RegistrationRequestDto approveRequest(String requestUuid) {
return handleApprove(request);
}

public void setValidationService(RegistrationRequestValidationService validationService) {
this.validationService = validationService;
}

public RegistrationRequestValidationService getValidationService() {
return validationService;
}

}
1 change: 1 addition & 0 deletions iam-login-service/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ iam:
authentication-type: ${IAM_REGISTRATION_AUTHENTICATION_TYPE:oidc}
oidc-issuer: ${IAM_REGISTRATION_OIDC_ISSUER:https://example.org}
saml-entity-id: ${IAM_REGISTRATION_SAML_ENTITY_ID:urn:example}
add-nickname-as-attribute: ${IAM_ADD_NICKNAME_AS_ATTRIBUTE:false}

local-authn:
login-page-visibility: ${IAM_LOCAL_AUTHN_LOGIN_PAGE_VISIBILITY:visible}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
package it.infn.mw.iam.test.registration;

import static it.infn.mw.iam.authn.ExternalAuthenticationHandlerSupport.EXT_AUTH_ERROR_KEY;
import static it.infn.mw.iam.registration.DefaultRegistrationRequestService.NICKNAME_ATTRIBUTE_KEY;
import static it.infn.mw.iam.test.ext_authn.oidc.OidcTestConfig.TEST_OIDC_CLIENT_ID;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
Expand Down Expand Up @@ -114,34 +116,31 @@ public void externalOidcRegistrationCreatesDisabledAccount() throws Exception {
// If the user tries to authenticate with his external account, he's redirected to the
// login page with an account disabled error

MockHttpSession session =
(MockHttpSession) mvc
.perform(get("/openid_connect_login").with(
SecurityMockMvcRequestPostProcessors.authentication(anonymousAuthenticationToken())))
.andExpect(status().isFound())
.andExpect(MockMvcResultMatchers
.redirectedUrlPattern(OidcTestConfig.TEST_OIDC_AUTHORIZATION_ENDPOINT_URI + "**"))
.andReturn()
.getRequest()
.getSession();
MockHttpSession session = (MockHttpSession) mvc
.perform(get("/openid_connect_login")
.with(SecurityMockMvcRequestPostProcessors.authentication(anonymousAuthenticationToken())))
.andExpect(status().isFound())
.andExpect(MockMvcResultMatchers
.redirectedUrlPattern(OidcTestConfig.TEST_OIDC_AUTHORIZATION_ENDPOINT_URI + "**"))
.andReturn()
.getRequest()
.getSession();

String state = (String) session.getAttribute("state");
String nonce = (String) session.getAttribute("nonce");

oidcProvider.prepareTokenResponse(TEST_OIDC_CLIENT_ID, TEST_100_USER, nonce);

session =
(MockHttpSession) mvc
.perform(get("/openid_connect_login").param("state", state)
.param("code", "1234")
.session(session))
.andExpect(status().isFound())
.andExpect(MockMvcResultMatchers.redirectedUrlPattern("/login**"))
.andExpect(
MockMvcResultMatchers.request().sessionAttribute(EXT_AUTH_ERROR_KEY, notNullValue()))
.andReturn()
.getRequest()
.getSession();
session = (MockHttpSession) mvc
.perform(
get("/openid_connect_login").param("state", state).param("code", "1234").session(session))
.andExpect(status().isFound())
.andExpect(MockMvcResultMatchers.redirectedUrlPattern("/login**"))
.andExpect(
MockMvcResultMatchers.request().sessionAttribute(EXT_AUTH_ERROR_KEY, notNullValue()))
.andReturn()
.getRequest()
.getSession();

assertThat(session.getAttribute(EXT_AUTH_ERROR_KEY), instanceOf(DisabledException.class));

Expand All @@ -152,33 +151,30 @@ public void externalOidcRegistrationCreatesDisabledAccount() throws Exception {
// the same happens after having confirmed the request
mvc.perform(get("/registration/confirm/{token}", token)).andExpect(status().isOk());

session =
(MockHttpSession) mvc
.perform(get("/openid_connect_login").with(
SecurityMockMvcRequestPostProcessors.authentication(anonymousAuthenticationToken())))
.andExpect(status().isFound())
.andExpect(MockMvcResultMatchers
.redirectedUrlPattern(OidcTestConfig.TEST_OIDC_AUTHORIZATION_ENDPOINT_URI + "**"))
.andReturn()
.getRequest()
.getSession();
session = (MockHttpSession) mvc
.perform(get("/openid_connect_login")
.with(SecurityMockMvcRequestPostProcessors.authentication(anonymousAuthenticationToken())))
.andExpect(status().isFound())
.andExpect(MockMvcResultMatchers
.redirectedUrlPattern(OidcTestConfig.TEST_OIDC_AUTHORIZATION_ENDPOINT_URI + "**"))
.andReturn()
.getRequest()
.getSession();

state = (String) session.getAttribute("state");
nonce = (String) session.getAttribute("nonce");

oidcProvider.prepareTokenResponse(TEST_OIDC_CLIENT_ID, TEST_100_USER, nonce);

session =
(MockHttpSession) mvc
.perform(get("/openid_connect_login").param("state", state)
.param("code", "1234")
.session(session))
.andExpect(status().isFound())
.andExpect(MockMvcResultMatchers.redirectedUrlPattern("/login**"))
.andExpect(request().sessionAttribute(EXT_AUTH_ERROR_KEY, notNullValue()))
.andReturn()
.getRequest()
.getSession();
session = (MockHttpSession) mvc
.perform(
get("/openid_connect_login").param("state", state).param("code", "1234").session(session))
.andExpect(status().isFound())
.andExpect(MockMvcResultMatchers.redirectedUrlPattern("/login**"))
.andExpect(request().sessionAttribute(EXT_AUTH_ERROR_KEY, notNullValue()))
.andReturn()
.getRequest()
.getSession();

assertThat(session.getAttribute(EXT_AUTH_ERROR_KEY), instanceOf(DisabledException.class));
err = (DisabledException) session.getAttribute(EXT_AUTH_ERROR_KEY);
Expand All @@ -187,6 +183,7 @@ public void externalOidcRegistrationCreatesDisabledAccount() throws Exception {

IamAccount account = iamAccountRepo.findByOidcId(OidcTestConfig.TEST_OIDC_ISSUER, TEST_100_USER)
.orElseThrow(() -> new AssertionError("Expected account not found"));
assertTrue(account.getAttributeByName(NICKNAME_ATTRIBUTE_KEY).isEmpty());

iamAccountRepo.delete(account);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/**
* Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.infn.mw.iam.test.registration;

import static it.infn.mw.iam.registration.DefaultRegistrationRequestService.NICKNAME_ATTRIBUTE_KEY;
import static it.infn.mw.iam.test.ext_authn.saml.SamlAuthenticationTestSupport.DEFAULT_IDP_ID;
import static it.infn.mw.iam.test.ext_authn.saml.SamlAuthenticationTestSupport.T1_EPUID;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import com.fasterxml.jackson.databind.ObjectMapper;

import it.infn.mw.iam.authn.saml.util.Saml2Attribute;
import it.infn.mw.iam.persistence.model.IamAccount;
import it.infn.mw.iam.persistence.model.IamSamlId;
import it.infn.mw.iam.persistence.repository.IamAccountRepository;
import it.infn.mw.iam.registration.RegistrationRequestDto;
import it.infn.mw.iam.test.api.TestSupport;
import it.infn.mw.iam.test.ext_authn.oidc.OidcTestConfig;
import it.infn.mw.iam.test.util.WithMockOIDCUser;
import it.infn.mw.iam.test.util.WithMockSAMLUser;
import it.infn.mw.iam.test.util.annotation.IamMockMvcIntegrationTest;
import it.infn.mw.iam.test.util.oauth.MockOAuth2Filter;


@RunWith(SpringRunner.class)
@IamMockMvcIntegrationTest
@TestPropertySource(properties = {"iam.registration.add-nickname-as-attribute=true"})
public class RegistrationAttributesTests extends TestSupport {

@Autowired
private ObjectMapper objectMapper;

@Autowired
private MockOAuth2Filter oauth2Filter;

@Autowired
private MockMvc mvc;

@Autowired
private IamAccountRepository iamAccountRepo;

@Before
public void setup() {
oauth2Filter.cleanupSecurityContext();
}

@After
public void teardown() {
oauth2Filter.cleanupSecurityContext();
}

private RegistrationRequestDto createRegistrationRequest() {

String username = "test-attributes";

String email = username + "@example.org";
RegistrationRequestDto request = new RegistrationRequestDto();
request.setGivenname("Test");
request.setFamilyname("User");
request.setEmail(email);
request.setUsername(username);
request.setNotes("Some short notes...");
request.setPassword("password");

return request;
}

@Test
public void linkAttributesFromLocalRegistrationRequest() throws Exception {

RegistrationRequestDto r = createRegistrationRequest();
mvc
.perform(post("/registration/create").contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(r)))
.andExpect(status().isOk());

IamAccount account = iamAccountRepo.findByEmail("[email protected]")
.orElseThrow(() -> new AssertionError("Expected account not found"));
assertTrue(account.getAttributeByName(NICKNAME_ATTRIBUTE_KEY)
.get()
.getValue()
.equals("test-attributes"));

iamAccountRepo.delete(account);

}

@Test
@WithMockOIDCUser(subject = TEST_100_USER, issuer = OidcTestConfig.TEST_OIDC_ISSUER)
public void linkAttributesFromOidcRegistrationRequest() throws Exception {

String username = "test-oidc-subject";

String email = username + "@example.org";
RegistrationRequestDto request = new RegistrationRequestDto();
request.setGivenname("Test");
request.setFamilyname("User");
request.setEmail(email);
request.setUsername(username);
request.setNotes("Some short notes...");

mvc
.perform(post("/registration/create").contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(request)))
.andExpect(status().isOk());

IamAccount account = iamAccountRepo.findByOidcId(OidcTestConfig.TEST_OIDC_ISSUER, TEST_100_USER)
.orElseThrow(() -> new AssertionError("Expected account not found"));
assertTrue(
account.getAttributeByName(NICKNAME_ATTRIBUTE_KEY).get().getValue().equals(username));

iamAccountRepo.delete(account);
}

@Test
@WithMockSAMLUser(issuer = DEFAULT_IDP_ID, subject = T1_EPUID)
public void linkAttributesFromSamlRegistrationRequest() throws Throwable {

String username = "test-saml-ext-reg";

String email = username + "@example.org";
RegistrationRequestDto request = new RegistrationRequestDto();
request.setGivenname("Test");
request.setFamilyname("Saml User");
request.setEmail(email);
request.setUsername(username);
request.setNotes("Some short notes...");

mvc
.perform(post("/registration/create").contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(request)))
.andExpect(status().isOk());

IamSamlId id = new IamSamlId(DEFAULT_IDP_ID, Saml2Attribute.EPUID.getAttributeName(), T1_EPUID);

IamAccount account = iamAccountRepo.findBySamlId(id)
.orElseThrow(() -> new AssertionError("Expected account not found"));
assertTrue(
account.getAttributeByName(NICKNAME_ATTRIBUTE_KEY).get().getValue().equals(username));

iamAccountRepo.delete(account);
}

}
Loading
Loading