4.4.3. Custom Authentication

Authentication mechanisms can provide access tokens by key, link, LDAP login and password, etc. REST API uses its own authentication mechanism that cannot be modified. In order to use custom authentication process, you need to create a REST controller and use its URL.

Let’s consider the custom authentication mechanism that enables getting an OAuth token by a promo code. In the following example we will use a sample application that contains the Coupon entity with code attribute. We will send this attribute’s value as an authentication parameter in GET request.

  1. Create a Coupon entity with the code attribute:

    @Column(name = "CODE", unique = true, length = 4)
    protected String code;
  2. Create a user with promo-user login on behalf of which the authentication will be performed.

  3. Create a new Spring configuration file with name rest-dispatcher-spring.xml under the root package (com.company.demo) of web module. The content of the file must be as follows:

    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
    
        <context:component-scan base-package="com.company.demo.web.rest"/>
    
    </beans>
  4. Include the file into the cuba.restSpringContextConfig application property in the modules/web/src/web-app.properties file:

    cuba.restSpringContextConfig = +com/company/demo/rest-dispatcher-spring.xml
  5. Create the rest package under the root package of web module and implement the custom Spring MVC controller in it. Use the OAuthTokenIssuer bean to generate and issue the REST API token for a user after the custom authentication:

    @RestController
    @RequestMapping("auth-code")
    public class AuthCodeController {
        @Inject
        private OAuthTokenIssuer oAuthTokenIssuer;
        @Inject
        private LoginService loginService;
        @Inject
        private Configuration configuration;
        @Inject
        private DataManager dataManager;
        @Inject
        private MessageTools messageTools;
    
        // here we check secret code and issue token using OAuthTokenIssuer
        @RequestMapping(method = RequestMethod.GET)
        public ResponseEntity get(@RequestParam("code") String authCode) {
            // obtain system session to be able to call middleware services
            WebAuthConfig webAuthConfig = configuration.getConfig(WebAuthConfig.class);
            UserSession systemSession;
            try {
                systemSession = loginService.getSystemSession(webAuthConfig.getTrustedClientPassword());
            } catch (LoginException e) {
                throw new RuntimeException("Error during system auth");
            }
    
            // set security context
            AppContext.setSecurityContext(new SecurityContext(systemSession));
            try {
                // find coupon with code
                LoadContext<Coupon> loadContext = LoadContext.create(Coupon.class)
                        .setQuery(LoadContext.createQuery("select c from demo$Coupon c where c.code = :code")
                                .setParameter("code", authCode));
    
                if (dataManager.load(loadContext) == null) {
                    // if coupon is not found - code is incorrect
                    return new ResponseEntity<>(new ErrorInfo("invalid_grant", "Bad credentials"), HttpStatus.BAD_REQUEST);
                }
    
                // generate token for "promo-user"
                OAuthTokenIssuer.OAuth2AccessTokenResult tokenResult =
                        oAuthTokenIssuer.issueToken("promo-user", messageTools.getDefaultLocale(), Collections.emptyMap());
                OAuth2AccessToken accessToken = tokenResult.getAccessToken();
    
                // set security HTTP headers to prevent browser caching of security token
                HttpHeaders headers = new HttpHeaders();
                headers.set(HttpHeaders.CACHE_CONTROL, "no-store");
                headers.set(HttpHeaders.PRAGMA, "no-cache");
                return new ResponseEntity<>(accessToken, headers, HttpStatus.OK);
            } finally {
                // clean up security context
                AppContext.setSecurityContext(null);
            }
        }
    
        // POJO for JSON error messages
        public static class ErrorInfo implements Serializable {
            private String error;
            private String error_description;
    
            public ErrorInfo(String error, String error_description) {
                this.error = error;
                this.error_description = error_description;
            }
    
            public String getError() {
                return error;
            }
    
            public String getError_description() {
                return error_description;
            }
        }
    }
  6. Exclude the rest package from scanning in web/core modules: the OAuthTokenIssuer bean is available only in REST API context, and scanning for it in the application context will cause an error.

    <context:component-scan base-package="com.company.demo">
        <context:exclude-filter type="regex" expression="com\.company\.demo\.web\.rest\..*"/>
    </context:component-scan>
  7. Now users will be able to obtain OAuth2 access code using GET HTTP request with the code parameter to

    http://localhost:8080/app/rest/auth-code?code=A325

    The result will be:

    {"access_token":"74202587-6c2b-4d74-bcf2-0d687ea85dca","token_type":"bearer","expires_in":43199,"scope":"rest-api"}

    The obtained access token should then be passed to REST API, as described in the documentation.