Wednesday, December 2, 2015

A tutorial on how to use the Tapestry-Security module Part 2

We pick up where we left off.

We start with our AppModule and configure our security service to use our "custom" realm.

    
    // @see http://tynamo.org/tapestry-security+guide
    @Contribute(WebSecurityManager.class)
    public static void addRealms(Configuration configuration, @InjectService("MiscAppRealm") AuthorizingRealm userRealm) {
        configuration.add(userRealm);
    }

With this we then update our MiscRealm.java code:
    
  @Inject
  private UserMembershipDAO umDAO;    

  public MiscAppRealm() {
        super(new MemoryConstrainedCacheManager());
        setAuthenticationTokenClass(UsernamePasswordToken.class);
        setCredentialsMatcher(new MiscRealmCredentialMatcher());
  }

  @Override
  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection pc) {
        if(pc == null) throw new AuthenticationException("PrincipalCollection was null, which should not happen");
        
        if(pc.isEmpty()) return null;
        
        if(pc.fromRealm(getName()).size() <= 0) return null;
        
        String loginName = (String)pc.fromRealm(getName()).iterator().next();
        if(loginName == null) return null;
                
        AppUsers user = umDAO.FindAppUsersByLoginId(loginName);
        
        if(user == null) return null;
                
        Set roles = new HashSet<>(user.getRolesList().size());
        user.getRolesList().stream().forEach((role) -> {
            roles.add(role.getDescription());
        });
        
        System.out.println("Roles: " + roles);
        
        return new SimpleAuthorizationInfo(roles);
  }

  @Override
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken at) throws AuthenticationException {                
        UsernamePasswordToken token = (UsernamePasswordToken)at;
        
        String username = token.getUsername();
        
        if(username == null){
            throw new AccountException("Null usernames are not allowed by this realm");
        }
        
        AppUsers user = umDAO.FindAppUsersByLoginId(username);
                
        if(user.getIsLocked()){
            throw new LockedAccountException("Account: " + username + " is locked.");
        }
         
        return new SimpleAuthenticationInfo(username, user.getEncodedPassword(), new SimpleByteSource(user.getPasswordSalt()), getName());
 }

We then move on to our custom credential matcher (MiscRealmCredentialMatcher.java) which extends from SimpleCredentialsMatcher. The thing there is overriding the doCredentialsMatch() method on it. You need to remember though is that the UserMembership table stores the encrypted password instead as plain text. You'll get the idea when you see the jasypt documentation.

@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
     
     AppUsers target = FindAppUsersByLoginId(UserId);
     String encryptedPassword = target.getPassword();
        
     BasicPasswordEncryptor passwordEncryptor = new BasicPasswordEncryptor();
        
     return passwordEncryptor.checkPassword(String.valueOf(token.getPrincipal()), encryptedPassword);
}

The last part of the puzzle is how we handle the Login form when we submit it. The Tynamo-Security's own code is helpful here.

    
void onValidateFromLogin() throws ValidationException {
        
     Subject currentUser = securityService.getSubject();
                
     if(currentUser == null){
         throw new IllegalStateException("Subject can`t be null");
     }
        
     UsernamePasswordToken token = new UsernamePasswordToken(loginId, password);
     token.setRememberMe(rememberMe);
        
     try {
          currentUser.login(token);
            
     } catch (UnknownAccountException e) {
         login.recordError(loginIdField, "");
         login.recordError(passwordField, "Invalid user name or password.");
     } catch (IncorrectCredentialsException e) {
         login.recordError(loginIdField, "");
         login.recordError(passwordField, "Invalid user name or password.");
     } catch (LockedAccountException e) {
         login.recordError(loginIdField, "Account is Locked. Please see Administrator");
     } catch (AuthenticationException e) {
         login.recordError(loginIdField, "");
         login.recordError(passwordField, "Invalid user name or password.");
     }
}

Object onSuccessFromLogin() throws MalformedURLException, IOException {
    if (StringUtils.hasText(successURL)) {
        if ("^".equals(successURL)) {
             return pageRenderLinkSource.createPageRenderLink(componentResources.getPage().getClass());
        }
        return new URL(successURL);
    }

    if (redirectToSavedUrl) {
         String requestUri = loginContextService.getSuccessPage();
         if (!requestUri.startsWith("/") && !requestUri.startsWith("http")) {
             requestUri = "/" + requestUri;
         }
         loginContextService.redirectToSavedRequest(requestUri);
        return null;
    }
    return loginContextService.getSuccessPage();
}

What's left is to annotate the pages to secure.

Edit: smallish changes to code