Differences
This shows you the differences between two versions of the page.
Both sides previous revision Previous revision Next revision | Previous revision | ||
tutorial:dev:add_authentication_method [2017/11/04 23:37] poulm |
tutorial:dev:add_authentication_method [2021/01/19 14:15] apeterova fixme - validate |
||
---|---|---|---|
Line 1: | Line 1: | ||
+ | ====== Authentication - create a new authentication method ====== | ||
+ | This tutorial shows, how to add a new authentication method to CzechIdM. The authentication will be typically done by some external authority, such as OpenAM, OAuth, Facebook, Google etc. | ||
+ | |||
+ | Supported authentication methods are implemented in several [[devel: | ||
+ | |||
+ | Something different holds for SSO. If you want the users to come to CzechIdM and be immediately logged in without the need to fill in any credentials (or be redirected to some other login page of e.g. Facebook), you need to implement a new IdmAuthenticationFilter (see [[devel: | ||
+ | |||
+ | A combination of both situations is possible, e.g. the [[tutorial: | ||
+ | |||
+ | ===== Create a new authenticator ===== | ||
+ | |||
+ | ==== Step 1 - Choose the module ==== | ||
+ | |||
+ | Your authenticator must belong to some backend module. You can create a new module, or choose an existing one. The authenticator will be called during authentication process only when the module is enabled. | ||
+ | |||
+ | If you decide to create a new module, please follow the [[tutorial: | ||
+ | |||
+ | ==== Step 2 - Create the Authenticator class ==== | ||
+ | |||
+ | Now you create a new Spring component, which implements the [[https:// | ||
+ | |||
+ | After you create the new class, you should: | ||
+ | * Add the '' | ||
+ | * Add the annotation '' | ||
+ | * Implement the interface method '' | ||
+ | * Implement the interface method '' | ||
+ | * Implement the interface method '' | ||
+ | * Implement the interface method '' | ||
+ | |||
+ | After this, your class would look similar to the following code (the example is taken from the [[tutorial: | ||
+ | |||
+ | <code java> | ||
+ | package eu.bcvsolutions.idm.openam.authentication.impl; | ||
+ | |||
+ | // ... imports | ||
+ | |||
+ | @Component(" | ||
+ | @Enabled(OpenAMModuleDescriptor.MODULE_ID) | ||
+ | @Description(" | ||
+ | public class OpenAMAuthenticator extends AbstractAuthenticator implements Authenticator { | ||
+ | |||
+ | private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(OpenAMAuthenticator.class); | ||
+ | |||
+ | private static final String AUTHENTICATOR_NAME = " | ||
+ | |||
+ | @Override | ||
+ | public String getName() { | ||
+ | return OpenAMAuthenticator.AUTHENTICATOR_NAME; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public String getModule() { | ||
+ | return EntityUtils.getModule(this.getClass()); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public int getOrder() { | ||
+ | return DEFAULT_AUTHENTICATOR_ORDER - 10; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public LoginDto authenticate(LoginDto loginDto) { | ||
+ | // TODO add implementation | ||
+ | return null; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public AuthenticationResponseEnum getExceptedResult() { | ||
+ | return AuthenticationResponseEnum.SUFFICIENT; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Step 3 - Implement the authentication method ==== | ||
+ | |||
+ | Now you must add the main thing - the implementation of the method '' | ||
+ | |||
+ | FIXME Since 10.7, the method validate should be also implemented. | ||
+ | |||
+ | The [[https:// | ||
+ | |||
+ | If the credentials are not correct, the method '' | ||
+ | |||
+ | If you successfully validate user name and password with your authentication method, you will need to find IdM identity and create JWT token, which will be used for following requests of the user. For getting the user by username, use the [[https:// | ||
+ | |||
+ | The example implementation of autowiring the services and implementing the '' | ||
+ | <code java> | ||
+ | private final IdmIdentityService identityService; | ||
+ | |||
+ | private final JwtAuthenticationService jwtAuthenticationService; | ||
+ | |||
+ | private final OpenAMAuthenticationService openAMAuthenticationService; | ||
+ | |||
+ | @Autowired | ||
+ | public OpenAMAuthenticator(IdmIdentityService identityService, | ||
+ | JwtAuthenticationService jwtAuthenticationService, | ||
+ | OpenAMAuthenticationService openAMAuthenticationService) { | ||
+ | super(); | ||
+ | |||
+ | Assert.notNull(identityService); | ||
+ | Assert.notNull(jwtAuthenticationService); | ||
+ | Assert.notNull(openAMAuthenticationService); | ||
+ | // | ||
+ | this.identityService = identityService; | ||
+ | this.jwtAuthenticationService = jwtAuthenticationService; | ||
+ | this.openAMAuthenticationService = openAMAuthenticationService; | ||
+ | } | ||
+ | |||
+ | // ... other implementation | ||
+ | |||
+ | @Override | ||
+ | public LoginDto authenticate(LoginDto loginDto) { | ||
+ | String username = loginDto.getUsername(); | ||
+ | GuardedString password = loginDto.getPassword(); | ||
+ | |||
+ | if (username == null || password == null) { | ||
+ | LOG.warn(" | ||
+ | return null; | ||
+ | } | ||
+ | |||
+ | String tokenId = openAMAuthenticationService.loginUserAndGetToken(username, | ||
+ | |||
+ | if (tokenId == null) { | ||
+ | LOG.info(" | ||
+ | return null; | ||
+ | } | ||
+ | |||
+ | LOG.info(" | ||
+ | |||
+ | // ... other implementation | ||
+ | |||
+ | IdmIdentityDto identity = identityService.getByUsername(username); | ||
+ | |||
+ | if (identity == null) { | ||
+ | throw new IdmAuthenticationException(MessageFormat.format( | ||
+ | " | ||
+ | } | ||
+ | |||
+ | return jwtAuthenticationService.createJwtAuthenticationAndAuthenticate( | ||
+ | loginDto, | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Step 4 - Implement the tests ==== | ||
+ | |||
+ | You should implement unit tests that would cover your new authenticator. You should have tests for your service, which implements the authentication logic. For the authenticator tests, you can mock the outputs of the services and test only the logic contained in the authenticator. | ||
+ | |||
+ | Example: | ||
+ | |||
+ | <code java> | ||
+ | package eu.bcvsolutions.idm.openam.authentication.impl; | ||
+ | |||
+ | // ... imports | ||
+ | |||
+ | public class OpenAMAuthenticatorTest extends AbstractUnitTest { | ||
+ | |||
+ | @Mock | ||
+ | private IdmIdentityService identityService; | ||
+ | |||
+ | @Mock | ||
+ | private JwtAuthenticationService jwtAuthenticationService; | ||
+ | |||
+ | @Mock | ||
+ | private OpenAMAuthenticationService openAMAuthenticationService; | ||
+ | |||
+ | private OpenAMAuthenticator openAMAuthenticator; | ||
+ | |||
+ | @Before | ||
+ | public void init() { | ||
+ | openAMAuthenticator = new OpenAMAuthenticator(identityService, | ||
+ | openAMAuthenticationService); | ||
+ | } | ||
+ | |||
+ | @Test | ||
+ | public void testAuthenticateSuccess() { | ||
+ | |||
+ | LoginDto loginDto = OpenAMTestUtil.createLoginDto(); | ||
+ | IdmIdentityDto idmIdentityDto = OpenAMTestUtil.createIdentityDto(); | ||
+ | |||
+ | when(openAMAuthenticationService.loginUserAndGetToken(loginDto.getUsername(), | ||
+ | |||
+ | when(identityService.getByUsername(TEST_USERNAME)).thenReturn(idmIdentityDto); | ||
+ | |||
+ | LoginDto responseLoginDto = new LoginDto(loginDto); | ||
+ | responseLoginDto.setAuthenticationModule(OpenAMModuleDescriptor.MODULE_ID); | ||
+ | when(jwtAuthenticationService.createJwtAuthenticationAndAuthenticate(eq(loginDto), | ||
+ | eq(OpenAMModuleDescriptor.MODULE_ID))).thenReturn(responseLoginDto); | ||
+ | |||
+ | LoginDto resultDto = openAMAuthenticator.authenticate(loginDto); | ||
+ | |||
+ | verify(openAMAuthenticationService).loginUserAndGetToken(loginDto.getUsername(), | ||
+ | |||
+ | verify(identityService).getByUsername(TEST_USERNAME); | ||
+ | |||
+ | Assert.assertNotNull(resultDto); | ||
+ | Assert.assertEquals(OpenAMModuleDescriptor.MODULE_ID, | ||
+ | Assert.assertEquals(TEST_USERNAME, | ||
+ | |||
+ | } | ||
+ | |||
+ | @Test | ||
+ | public void testAuthenticateInvalidPassword() { | ||
+ | LoginDto loginDto = OpenAMTestUtil.createLoginDto(); | ||
+ | |||
+ | when(openAMAuthenticationService.loginUserAndGetToken(loginDto.getUsername(), | ||
+ | |||
+ | LoginDto resultDto = openAMAuthenticator.authenticate(loginDto); | ||
+ | |||
+ | verify(openAMAuthenticationService).loginUserAndGetToken(loginDto.getUsername(), | ||
+ | |||
+ | Assert.assertNull(resultDto); | ||
+ | } | ||
+ | |||
+ | @Test | ||
+ | public void testAuthenticateMissingIdentity() { | ||
+ | |||
+ | LoginDto loginDto = OpenAMTestUtil.createLoginDto(); | ||
+ | |||
+ | when(openAMAuthenticationService.loginUserAndGetToken(loginDto.getUsername(), | ||
+ | |||
+ | when(identityService.getByUsername(TEST_USERNAME)).thenReturn(null); | ||
+ | |||
+ | Exception ex = null; | ||
+ | try { | ||
+ | openAMAuthenticator.authenticate(loginDto); | ||
+ | } catch (IdmAuthenticationException e) { | ||
+ | ex = e; | ||
+ | } | ||
+ | // Exception was returned because of non-existing identity | ||
+ | Assert.assertNotNull(ex); | ||
+ | |||
+ | verify(openAMAuthenticationService).loginUserAndGetToken(loginDto.getUsername(), | ||
+ | verify(identityService).getByUsername(TEST_USERNAME); | ||
+ | |||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Step 5 - Build and install ==== | ||
+ | |||
+ | Finally, build the module and install it to CzechIdM. Make sure your module is enabled in the Configuration. | ||
+ | |||
+ | Congratulations, | ||
+ | |||
+ | ===== Create a new authentication filter for SSO ===== | ||
+ | |||
+ | ==== Step 1 - Choose the module ==== | ||
+ | |||
+ | Your authentication filter must belong to some backend module. You can create a new module, or choose an existing one. The filter will be called during authentication process only when the module is enabled. | ||
+ | |||
+ | If you decide to create a new module, please follow the [[tutorial: | ||
+ | |||
+ | ==== Step 2 - Create the IdmAuthenticationFilter class ==== | ||
+ | |||
+ | Now you create a new Spring component, which implements the [[https:// | ||
+ | |||
+ | After you create the new class, you should: | ||
+ | * Add the '' | ||
+ | * Add the annotation '' | ||
+ | * Add the annotation '' | ||
+ | |||
+ | After this, your class would look similar to the following code (the example is taken from the [[tutorial: | ||
+ | |||
+ | <code java> | ||
+ | package eu.bcvsolutions.idm.openam.authentication.filter; | ||
+ | |||
+ | // ... imports | ||
+ | |||
+ | @Order(10) | ||
+ | @Component(" | ||
+ | @Enabled(OpenAMModuleDescriptor.MODULE_ID) | ||
+ | public class OpenAMIdmAuthenticationFilter implements IdmAuthenticationFilter { | ||
+ | |||
+ | private static final Logger LOG = LoggerFactory.getLogger(OpenAMIdmAuthenticationFilter.class); | ||
+ | |||
+ | @Override | ||
+ | public boolean authorize(String token, HttpServletRequest req, HttpServletResponse res) { | ||
+ | // TODO add implementation | ||
+ | return false; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Step 4 - Specify headers and implement the authentication method ==== | ||
+ | |||
+ | The filter will be called only when the HTTP request contains specified headers. You must know which header contains tokens for your authentication method and return its name in the overriden method '' | ||
+ | |||
+ | You can also override the method '' | ||
+ | |||
+ | Now you must add the main thing - the implementation of the method '' | ||
+ | |||
+ | The input variable '' | ||
+ | |||
+ | If the authentication is not successful, the method '' | ||
+ | |||
+ | If you successfully validate the user with your authentication method, you will need to find IdM identity and create JWT token, which will be used for following requests of the user. For getting the user, obtain their username from the external authority, and use the [[https:// | ||
+ | |||
+ | The example implementation of autowiring the services and implementing the interface methods follows. Note that it's recommended to implement your authentication method in a separate service (here '' | ||
+ | <code java> | ||
+ | @Autowired | ||
+ | private OpenAMTokenValidationService openAMTokenValidationService; | ||
+ | |||
+ | @Autowired | ||
+ | private IdmIdentityService identityService; | ||
+ | |||
+ | @Autowired | ||
+ | private JwtAuthenticationService jwtAuthenticationService; | ||
+ | |||
+ | @Override | ||
+ | public boolean authorize(String token, HttpServletRequest req, HttpServletResponse res) { | ||
+ | try { | ||
+ | String tokenId = openAMTokenValidationService.retrieveTokenId(req); | ||
+ | |||
+ | if (tokenId == null) { | ||
+ | LOG.debug(" | ||
+ | return false; | ||
+ | } | ||
+ | |||
+ | String userName = openAMTokenValidationService.retrieveUserNameForToken(tokenId, | ||
+ | |||
+ | if (userName == null) { | ||
+ | // Remove invalid cookie so next requests won't need to try validation again | ||
+ | openAMTokenValidationService.removeTokenFromHeaders(res); | ||
+ | |||
+ | LOG.info(" | ||
+ | return false; | ||
+ | } | ||
+ | |||
+ | LOG.info(" | ||
+ | |||
+ | IdmIdentityDto identity = identityService.getByUsername(userName); | ||
+ | |||
+ | if (identity == null) { | ||
+ | throw new IdmAuthenticationException(MessageFormat.format( | ||
+ | " | ||
+ | + "[{0}] either doesn' | ||
+ | userName)); | ||
+ | } | ||
+ | |||
+ | LoginDto loginDto = createLoginDto(userName); | ||
+ | |||
+ | LoginDto fullLoginDto = jwtAuthenticationService.createJwtAuthenticationAndAuthenticate(loginDto, | ||
+ | identity, | ||
+ | |||
+ | return fullLoginDto != null; | ||
+ | |||
+ | } catch (IdmAuthenticationException e) { | ||
+ | LOG.warn(" | ||
+ | } catch (Exception e) { | ||
+ | LOG.error(" | ||
+ | } | ||
+ | |||
+ | return false; | ||
+ | } | ||
+ | |||
+ | private LoginDto createLoginDto(String userName) { | ||
+ | LoginDto ldto = new LoginDto(); | ||
+ | ldto.setUsername(userName); | ||
+ | return ldto; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public String getAuthorizationHeaderName() { | ||
+ | return " | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public String getAuthorizationHeaderPrefix() { | ||
+ | return ""; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Step 4 - Implement the tests ==== | ||
+ | |||
+ | You should implement unit tests that would cover your new filter. You should have tests for your service, which implements the authentication logic. For the filter tests, you can mock the outputs of the services and test only the logic contained in the filter. | ||
+ | |||
+ | ==== Step 5 - Build and install ==== | ||
+ | |||
+ | Finally, build the module and install it to CzechIdM. Make sure your module is enabled in the Configuration. |