REST authentication password hashes

Hi,

I implemented in a very rough manner a custom External database authentication, taking as example LDAP Authentication and custom authentication (Custom Authentication - CUBA Platform. Developer’s Manual, using platform 6.10 for existing custom WEB Application.

I need to ‘Appify’ an existing custom application , so existing users are contained in a table of custom application
and say I know algorithm that generate hashes from cleartext passwords.
I created a main datastore for CUBA with web and Polymer application modules, which is compiled as an APP using Cordova (android only, local testing) or Adobe cloud service (Phonegap).
An additional Datastore contains application tables in a different Mysql Database/Schema, accessible readonly and containing table needed for operations. On behalf of authentication Users table with login/password (hashes), Persons (one-to-one to Users), descriptive of credentials (First Name, Last Name, email, etc).

I created a Cuba Framework User (say: app-user) with administrative privileges in framework (say copy of admin), but i do not want to replicate custom application users, only “substitute” each authenticated application users after login OK and token generated, saving in APPContext (as attributes) information needed for further processing (including external user properties as First Name,LastName, IDs,email, and so on, have a look at MVC Controller code, before generating token)

So I created a SpringMVC controller as in: Custom Authentication - CUBA Platform. Developer’s Manual, a service which hashes cleartext password and search/compares credentials, returning OK to SpeingMVC Controller which generate token.
I want share code because I think it’s a recurring use case.

Some code Enhancement are possible:

a. creating an Interface with single method (say generateHash(map<String,Object> parameters) and bean implementations to create password hashes for different algorithms in order to decouple password hashing function

b. review Exceptions

c. increase security using:

  1. cuba.trustedClientPassword for anonymous session MVC controller. I tried this (in local-app.properties) but
    no more anonymous initial auth was possible in web module (framework not started correctly, why?)

  2. cuba.trustedClientPermittedIpList = 127.0.0.1, 10.17..
    Default value: 127.0.0.1
    for APP probably do not make sense, because IP is not predictable, differently for Polymer mobile site
    (I didn’t do extensive testing with curl on different IP than localhost)

  3. I do not understand if cuba.rest.client.id/cuba.rest.client.secret are still needed after custom authentication
    and token generation or if they are needed only to first time access of custom REST url for authentication
    (@RequestMapping(value = "/v2/extdb/token") in MVC controller

  4. cuba.rest.standardAuthenticationUsers can be used to exclude users from REST auth (es. admin user), but it
    does not seem to work (have a look at commented code in MVC)


     /*
     if (restApiConfig.getStandardAuthenticationUsers().contains(username)) {
         log.info("User {} is not allowed to use external login in REST API", username);
         throw new BadCredentialsException("Bad credentials");
     }
     */
  1. I thougth to introduce similar solution in cuba-login.html in Polymer code, but I do not want to modify code
    provided in order to preserve modification between updates/upgrades. Ho can I do it?
      is: 'cuba-login',

      behaviors: [CubaLocalizeBehavior, CubaAppAwareBehavior],

      properties: {

        /**
         * Set to true to use LDAP authentication endpoint
         */
        ldap: {
          type: Boolean,
          value: false
        },

        _keysTarget: {
          type: Object,
          value: function () {
            return this.querySelector('.fields input');
          }
        }
      }

 (a flag and code to enable/disable extdb Auth in cuba-polymer javascript modules)
  1. using SSL in comunications

Thank you for your patience in revising some of my doubth about framework authentications insights.

I think it’s a common use case so code can be reused and improved.

Bye,
Fabrizio

Spring MVC Controller:

@RestController
public class ExternalDBAuthController implements InitializingBean {
    private static final Logger log = LoggerFactory.getLogger(ExternalDBAuthController.class);

    @Inject
    private TrustedClientService trustedClientService;

    @Inject
    private MessageTools messageTools;

    @Inject
    protected Configuration configuration;

    @Inject
    protected OAuthTokenIssuer oAuthTokenIssuer;

    @Inject
    protected CheckExtPasswordService passwordService;

    @Inject
    protected RestApiConfig restApiConfig;

    private Locale locale;
    private String username;
    private String ipAddress;
    private String password;

    protected Set<HttpMethod> allowedRequestMethods = Collections.singleton(HttpMethod.POST);

    protected WebResponseExceptionTranslator providerExceptionHandler = new DefaultWebResponseExceptionTranslator();

    @RequestMapping(value = "/v2/extdb/token", method = RequestMethod.GET)
    public ResponseEntity<OAuth2AccessToken> getAccessToken(Principal principal,
                                                            @RequestParam Map<String, String> parameters,
                                                            HttpServletRequest request)
            throws HttpRequestMethodNotSupportedException {
        if (!allowedRequestMethods.contains(HttpMethod.GET)) {
            throw new HttpRequestMethodNotSupportedException("GET");
        }

        return postAccessToken(principal, parameters, request);
    }

    @RequestMapping(value = "/v2/extdb/token", method = RequestMethod.POST)
    public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal,
                                                             @RequestParam Map<String, String> parameters,
                                                             HttpServletRequest request)
            throws HttpRequestMethodNotSupportedException {


        // FIXME: verify if there are real filters on IP for Anonymous user
        // FIXME: verify password for anonymous WEBAuth and REST client usage

        // Obtain Trusted Client Auth
        /*

        cuba.trustedClientPassword
            Defines password used by the LoginService.loginTrusted() method. The Middleware layer can authenticate users who connect via the trusted client block without checking the user password.
            This property is used when user passwords are not stored in the database, while the client block performs the actual authentication itself. For example, by integrating with Active Directory.

        cuba.trustedClientPermittedIpList
            Defines the list of IP addresses, from which the invocation of the LoginService.loginTrusted() method is allowed. For example:

            cuba.trustedClientPermittedIpList = 127.0.0.1, 10.17.*.*
            Default value: 127.0.0.1

            N.B.  LoginService.loginTrusted() Deprecated in 6.9 --> trustedClientService.getSystemSession();

        */

        // Autenticate as System User defined in configuration
        UserSession systemSession;
        WebAuthConfig webAuthConfig = configuration.getConfig(WebAuthConfig.class);

        try {
                systemSession = trustedClientService.getAnonymousSession(webAuthConfig.getTrustedClientPassword());
                AppContext.setSecurityContext(new SecurityContext(systemSession));

            } catch (LoginException e) {
                throw new RuntimeException("Error during system auth");
        }

        String grantType = parameters.get(OAuth2Utils.GRANT_TYPE);
        if (!"password".equals(grantType)) {
            throw new InvalidGrantException("grant type not supported for extdb endpoint");
        }

        locale = request.getLocale();
        username = parameters.get("username");
        ipAddress = request.getRemoteAddr();
        password = parameters.get("password");


        // Check if username is Excluded from Authentication in REST API
        /**
         * @return list of users that are not allowed to use external authentication. They can use only standard authentication.
         *         Empty list means that everyone is allowed to login using external authentication.
         */
        /*
        @Property("cuba.rest.standardAuthenticationUsers")
        @Factory(factory = CommaSeparatedStringListTypeFactory.class)
        List<String> getStandardAuthenticationUsers();
        */

        // FIXME: Exclusion of users fro REST Authentication using this module

        /*
        if (restApiConfig.getStandardAuthenticationUsers().contains(username)) {
            log.info("User {} is not allowed to use external login in REST API", username);
            throw new BadCredentialsException("Bad credentials");
        }
        */


        try {

            if (Strings.isNullOrEmpty(username)) {
                log.info("REST API authentication failed with empty login: {}", username, ipAddress);
                throw new BadCredentialsException("Bad credentials");
            }

            if (!passwordService.authenticate(username,password)) {
                log.info("REST API authentication failed with empty login: {}", username, ipAddress);
                throw new BadCredentialsException("Bad credentials");
            }

            // Generate Token for APP User
            OAuthTokenIssuer.OAuth2AccessTokenResult tokenResult = oAuthTokenIssuer.issueToken("app-logicway", locale, Collections.emptyMap());

            // Enrich UserSession  with ID,Username,Person Name,Surname from External Auth, respectvely: (extusername,extpassword,extfirstname,extlastname String, person_id,user_id Long)
            /*
            The UserSession object can contain named attributes of arbitrary serializable type.
            The attributes are set by setAttribute() method and returned by getAttribute() method. The latter is also able to return the following session parameters, as if they were attributes:

                userId – ID of the currently registered or substituted user;
                userLogin – login of the currently registered or substituted user in lowercase.
            */

            // FIXME: Configuration fo APP user

            UserSession appUserSession;
            appUserSession = tokenResult.getUserSession();
            appUserSession.setAttribute("extuser",username);
            appUserSession.setAttribute("extpassword",password);
            appUserSession.setAttribute("extIP",ipAddress);
            AppContext.setSecurityContext(new SecurityContext(appUserSession));

            // FIXME: Enrich appUserSession with CheckExtPasswordService

            return ResponseEntity.ok(tokenResult.getAccessToken());

        } finally {

        }


    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
        log.error("Exception in ExternalDB auth controller", e);
        return new ResponseEntity<>(new OAuth2Exception("Server error", e), HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler(BadCredentialsException.class)
    public ResponseEntity<OAuth2Exception> handleBadCredentialsException(BadCredentialsException e) throws Exception {
        return getExceptionTranslator().translate(e);
    }

    @ExceptionHandler(ClientAuthenticationException.class)
    public ResponseEntity<OAuth2Exception> handleClientAuthenticationException(ClientAuthenticationException e) throws Exception {
        return getExceptionTranslator().translate(e);
    }

    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ResponseEntity<OAuth2Exception> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) throws Exception {
        log.info("Handling error: {}, {}", e.getClass().getSimpleName(), e.getMessage());
        return getExceptionTranslator().translate(e);
    }

    public WebResponseExceptionTranslator getExceptionTranslator() {
        return providerExceptionHandler;
    }

    public void setExceptionTranslator(WebResponseExceptionTranslator providerExceptionHandler) {
        this.providerExceptionHandler = providerExceptionHandler;
    }

    public Set<HttpMethod> getAllowedRequestMethods() {
        return allowedRequestMethods;
    }


    public void setAllowedRequestMethods(Set<HttpMethod> allowedRequestMethods) {
        this.allowedRequestMethods = allowedRequestMethods;
    }


    @Override
    public void afterPropertiesSet() throws Exception {

    }

    // FIXME: Set/Check Essential Properties related to extdbAuth

    /*
    @Override
    public void afterPropertiesSet() throws Exception {
        this.ldapConfig = configuration.getConfig(RestLdapConfig.class);
        this.restApiConfig = configuration.getConfig(RestApiConfig.class);

        if (ldapConfig.getLdapEnabled()) {
            checkRequiredConfigProperties(ldapConfig);

            defaultLdapContextSource = createLdapContextSource(ldapConfig);
            defaultLdapTemplate = createLdapTemplate(defaultLdapContextSource);
            if (ldapContextSource == null) {
                ldapContextSource = defaultLdapContextSource;
            }
            if (ldapTemplate == null) {
                ldapTemplate = defaultLdapTemplate;
            }
            if (ldapUserLoginField == null) {
                ldapUserLoginField = ldapConfig.getLdapUserLoginField();
            }
        }
    }
    */
}

Authenication Service Code:

@Service(CheckExtPasswordService.NAME)
public class CheckExtPasswordServiceBean implements CheckExtPasswordService {

    @Inject
    Logger log;

    @Inject
    private Persistence persistence;

    @Inject
    private DataManager dataManager;



    @Override
    public boolean authenticate(String username, String password)  {
        MessageDigest md = null;
        byte[] passwordBytes = null;
        byte[] messageDigest = null;
        List<Utentiportale> utentiPortale;

        // FIXME: generare bean ed interfaccia per algoritmo generazione HASH

        // Calculate MD5 of Password
        try {
            md = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            log.debug("CheckExtPasswordService.authenticate()"+ e.getLocalizedMessage());
            return false;
        }

        try {
            passwordBytes = password.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            log.debug("CheckExtPasswordService.authenticate()"+ e.getLocalizedMessage());
            return false;
        }

        md.reset();
        try {
            md.update(passwordBytes);
        } catch (NullPointerException e) {
            log.debug("CheckExtPasswordService.authenticate()"+ e.getLocalizedMessage());
            return false;
        }

        messageDigest = md.digest();
        BigInteger bigInt = new BigInteger(1,messageDigest);
        String hashtext = bigInt.toString(16);
        while(hashtext.length() < 32 ){
            hashtext = "0"+hashtext;
        }

        // Search User/Password
        String queryStr ="select u from applogicway$Utentiportale u where u.login=:login and u.password=:password";

        LoadContext<Utentiportale> loadContext = LoadContext.create(Utentiportale.class)
                .setQuery(LoadContext.createQuery(queryStr)
                        .setParameter("login",username)
                        .setParameter("password",hashtext));

        utentiPortale = (List<Utentiportale>) dataManager.loadList(loadContext);

        if ((utentiPortale == null) || (utentiPortale.isEmpty()) || (utentiPortale.size() > 1)) {
            log.debug("CheckExtPasswordService.authenticate() - Credentials not Found");
            return  false;
        }

        return utentiPortale.get(0).getPassword().trim().equalsIgnoreCase(hashtext);
    }
}

Hi, unfortunately I’m not able to provide detailed analysis of your solution but here are a few points:

You should probably place you custom controller outside of /v2 path which is aimed to be used for already authenticated requests

I do not understand if cuba.rest.client.id/cuba.rest.client.secret are still needed after custom authentication

No, these are used only for built-in authentication.

cuba.trustedClientPermittedIpList = 127.0.0.1, 10.17. .
Default value: 127.0.0.1
for APP probably do not make sense, because IP is not predictable, differently for Polymer mobile site
(I didn’t do extensive testing with curl on different IP than localhost)

It is relevant only for Generic UI [client]

cuba.rest.standardAuthenticationUsers can be used to exclude users from REST auth (es. admin user), but it
does not seem to work (have a look at commented code in MVC)

It is relevant only for ldap authentication (see LdapAuthController)

I thougth to introduce similar solution in cuba-login.html in Polymer code, but I do not want to modify code
provided in order to preserve modification between updates/upgrades. Ho can I do it?

You will need to create your component.