Saturday, January 12, 2013

Writing REST Services in Java: Part 2 User sign up and Login

Previous Post : Part One: Introduction   Next Post : Part Three: Email Verification
Get the Code: https://github.com/iainporter/rest-java

In this post I will walk through the sign up and login services to demonstrate a vertical slice through the application. I will briefly discuss email verification and Social signup but won't delve into that until the next post. Session handling is also deferred until a later post.

User Sign up

To create a User we need to persist some basic information. At a bare minimum we need an email address and a password. Optionally we would like to capture perhaps a first and last name. 
Once a user has signed up we need them to verify their email address. This is important foremost so that we can uniquely identify the user and provide the ability for them to safely reset their password, as well as other security features that depend on a valid email address being provided.

Most services will follow a similar development pattern to this one which is:

1. Define the domain model
2. Write the Repository interface to persist and retrieve the domain objects
3. Write a Service class to map the API object to the entity and CRUD logic to integrate with the Repository, as well as any other services in support of the particular API.
4. Define the API 
5. Write the Resource Controller

The unit tests will test each layer of the application and the Integration Tests will test the full end to end calls through the application.

The User Domain Object

Entities are defined in the com.porterhead.rest.model package. Hibernate annotations are used to manage the joins and Spring Data to manage some of the basic functions common to all entities. All entities have an additional identifier which is a UUID. Some people have a preference to just use a sequential Long to identify an entity. In this application the primary keys are Longs but UUIDs are used in the API calls to persist and retrieve entities.

It comes down to whether you want your urls to look like this:

/user/1

or like this:

/user/38e1e415-acd2-42bb-8a3f-594f13e0d4ea




All entities that we wish to persist are annotated with:

@Entity
@Table(name="table name")

The class is added to the list of known entities in src/main/resources/META-INF/persistence.xml

The beans responsible for persistence are defined in src/main/resources/META-INF/spring/data-context.xml. There are bean profiles for dev, local, staging and production.

The User Repository

The repository tier uses Spring Data which is an excellent framework that takes care of most of the boilerplate headaches with CRUD functionality. The UserRepository class extends the JPARepository interface and has a few custom finder methods defined.

The User Service

The UserService interface defines a method for creating a new User:

public AuthenticatedUserToken createUser(CreateUserRequest request, Role role);

All entities remain within the boundary of the service layer and API objects pass between the Controllers and the services. This allows us to tightly define the contract between clients and the API. All request and responses are defined in the API package. It also prevents potential transaction and hibernate issues by accessing entity objects outside of a transaction. We can also pass API objects to multiple services without worrying about whether that service knows how to persist a particular entity.

It is worth stepping through the implementation of createUser as it will highlight several key features of how service methods are composed.

The method:

    @Transactional
    public AuthenticatedUserToken createUser(CreateUserRequest request, Role role) {
        validate(request);
        User searchedForUser = userRepository.findByEmailAddress(request.getUser().getEmailAddress());
        if (searchedForUser != null) {
            throw new DuplicateUserException();
        }

        User newUser = createNewUser(request, role);
        AuthenticatedUserToken token = new AuthenticatedUserToken(newUser.getUuid().toString(), createAuthorizationToken(newUser).getToken());
        userRepository.save(newUser);
        return token;
    }


First the API request is validated and if it fails an exception is thrown. This is a common pattern in service methods; to fail fast on an error. All Exceptions extend BaseWebApplicationException which defines an error payload for the response and an appropriate Http status code. In this particular case it will be a 400 error and in the case just below a 409 will be returned for the DuplicateUserException.

If no duplicate is found the a User entity is mapped from the request object and persisted.
Note that the password is saved as a one-way hash salted with the UUID of the User.

Finally an AuthenticatedUserToken object is mapped from the created entity and returned from the method.

See com.incept5.rest.service.UserServiceTest for the full suite of tests to run against this service.

The User Resource Controller

Resource classes are responsible for marshalling and unmarshalling client requests and responses and coordinating calls to services. Resource classes do not access entities but instead use objects in the com.incept5.rest.api package to communicate with services.

There are some JAX-RS annotations to map urls to the correct resource:


@Path("/user")
@Component
@Produces({MediaType.APPLICATION_JSON})
@Consumes({MediaType.APPLICATION_JSON})

The method for sign up is:


    @PermitAll
    @POST
    public Response signupUser(CreateUserRequest request) {
        AuthenticatedUserToken token = userService.createUser(request, Role.authenticated);
        verificationTokenService.sendEmailRegistrationToken(token.getUserId());
        URI location = uriInfo.getAbsolutePathBuilder().path(token.getUserId()).build();
        return Response.created(location).entity(token).build();
    }

We specify that it is a POST and the @PermitAll means that the method is not access controlled. In a later post I will walk through customising a sign up for different types of users (customer, merchant, etc). The general sign up just assigns an authenticated role to a new user.

The logic flow is:

1. Call the User Service to handle the create request. If it is successful an API object will be returned with details of the new User.
2. Call the Email Verification Service to send a welcome email along with an embedded link to verify the email address. The details of this will be discussed in the next post. Note that this an asynchronous call and will return as soon as the Verification Service puts the request onto its queue.
3. A location uri for the new resource is created. This takes the form of user/<uuid>. We will see this in the next section when the API is tested with a curl request.
4. The response is gathered and returned. The Http response will be created (201) and the body will contain an AuthenticatedUserToken. Again I will go into more detail about that later.

Testing the API for User sign up

There are in-memory unit tests in the com.porterhead.rest.resource package for testing the Resource classes  but the surest way to test it is by running the code in a servlet container and firing real requests at it.
The integration tests are in the src/test/groovy directory. The tests are written in groovy and executed using the groovy rest client. UserIntegrationTest has tests for both failure and success scenarios.

Testing the API with curl

To test with curl we need to start up the war in a servlet container. The default profile in gradle.properties is dev but if we wanted to test against a real database and a real mail sender we could change that to local or staging and configure the necessary beans. I will discuss how to do that later.
Execute the following command:

gradle tomcatRun

Once the war has finished loading we can execute the following curl request:

curl -v -H "Content-Type: application/json" -X POST -d '{"user":{"firstName":"Foo","lastName":"Bar","emailAddress":"<your email address here>"}, "password":"password"}' http://localhost:8080/java-rest/user

You should see a response like this:

< HTTP/1.1 201 Created
< Server: Apache-Coyote/1.1
< Location: http://localhost:8080/java-rest/user/ff7ffcf0-cfe0-4c1e-9971-3b934612b154
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sat, 12 Jan 2013 09:43:13 GMT
<  * Connection #0 to host localhost left intact
{"userId":"ff7ffcf0-cfe0-4c1e-9971-3b934612b154","token":"5d1cf32e-e468-4a2c-92b6-9b0c377145f9"}

I'll discuss how to use the response in the next post.

Now that we know sign up works we can test out login.
Try the following curl statement:

curl -v -H "Content-Type: application/json" -X POST -d '{"username":"<your email address here>","password":"password"}' http://localhost:8080/java-rest/user/login


The response:



< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< Content-Location: http://localhost:8080/java-rest/user/7ef646ba-5e03-4d59-90a9-822daecf3428
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sat, 12 Jan 2013 09:52:21 GMT
<
* Connection #0 to host localhost left intact
{"userId":"7ef646ba-5e03-4d59-90a9-822daecf3428","token":"164797a0-623f-427d-a469-b348e7e2bc59"}

That covers user sign up and login. In the next post I will discuss the email verification process.






31 comments:

  1. Any idea why I get a internal 500 error when testing with curl ?

    ReplyDelete
    Replies
    1. What gradle command did you use? And what was the curl statement that failed?

      Delete
  2. Sorry for the slow response. I'm running the example you put on this page :

    N:\GitHub> curl -v -H "Content-Type: application/json" -X POST -d '{"user":{"fir
    stName":"Foo","lastName":"Bar","emailAddress":""}, "pas
    sword":"password"}' http://localhost:8080/java-rest/user

    * Adding handle: conn: 0xa50368
    * Adding handle: send: 0
    * Adding handle: recv: 0
    * Curl_addHandleToPipeline: length: 1
    * - Conn 0 (0xa50368) send_pipe: 1, recv_pipe: 0
    * Could not resolve host: email
    * Closing connection 0
    curl: (6) Could not resolve host: email
    * Adding handle: conn: 0xa50368
    * Adding handle: send: 0
    * Adding handle: recv: 0
    * Curl_addHandleToPipeline: length: 1
    * - Conn 1 (0xa50368) send_pipe: 1, recv_pipe: 0
    * Could not resolve host: address
    * Closing connection 1
    curl: (6) Could not resolve host: address
    curl: (3) [globbing] unmatched close brace/bracket at pos 6
    * Adding handle: conn: 0xa5d3e0
    * Adding handle: send: 0
    * Adding handle: recv: 0
    * Curl_addHandleToPipeline: length: 1
    * - Conn 2 (0xa5d3e0) send_pipe: 1, recv_pipe: 0
    * About to connect() to localhost port 8080 (#2)
    * Trying 127.0.0.1...
    * Connected to localhost (127.0.0.1) port 8080 (#2)
    > POST /java-rest/user HTTP/1.1
    > User-Agent: curl/7.30.0
    > Host: localhost:8080
    > Accept: */*
    > Content-Type: application/json
    > Content-Length: 52
    >
    * upload completely sent off: 52 out of 52 bytes
    < HTTP/1.1 500 Internal Server Error
    * Server Apache-Coyote/1.1 is not blacklisted
    < Server: Apache-Coyote/1.1
    < Content-Type: text/html;charset=utf-8
    < Content-Length: 1063
    < Date: Fri, 18 Apr 2014 17:19:56 GMT
    < Connection: close

    ReplyDelete
    Replies
    1. or rather :

      curl -v -H "Content-Type: application/json" -X POST -d '{"user":{"firstName":"Foo","lastName":"Bar","emailAddress":"foobar@foobar.com"}, "password":"password"}' http://localhost:8080/java-rest/user

      Delete
  3. It looks like the error is caused by this on the server side :

    org.codehaus.jackson.JsonParseException: Unexpected character ('u' (code 117)): was expecting double-quote to start field name

    Not sure why curl is mangling the JSON data being sent ? (quote problem it looks like)

    ReplyDelete
  4. Ok, on windows, you have to replace the outside single quotes with double quotes, and escalte the inside double quotes , like this :

    curl -v -H "Content-Type: appli
    cation/json" -X POST -d "{\"user\":{\"firstName\":\"Foo\",\"lastName\":\"Bar\",\
    "emailAddress\":\"foobar@foobar.com\"}, \"password\":\"password\"}" http://l
    ocalhost:8080/java-rest/user

    ReplyDelete
  5. Iain... thanks! Really amazing post! Huge contribution!

    ReplyDelete
  6. can you provide source code with maven.
    I am execute gradle command gardle eclipse, this command execute successfully and also create eclipse project but when I import project in eclipse and run on server it will not run.

    ReplyDelete
  7. I assume you have installed the gradle plugin for eclipse - http://www.gradle.org/docs/current/userguide/eclipse_plugin.html

    If you have you should be able to execute any gradle command within eclipse that you can run from the command line.

    What gradle command is failing for you?

    ReplyDelete
  8. This comment has been removed by the author.

    ReplyDelete
  9. Thanks for quick replay
    but when I import in eclipse got following exception
    org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor#0': Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [META-INF/spring/data-context.xml]: Cannot resolve reference to bean 'dataSource' while setting bean property 'dataSource'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [META-INF/spring/data-context.xml]: Cannot resolve reference to bean 'database' while setting constructor argument; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'database' is defined
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:547) ~[spring-beans-4.0.0.M3.jar:4.0.0.M3]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:475) ~[spring-beans-4.0.0.M3.jar:4.0.0.M3]
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:299) ~[spring-beans-4.0.0.M3.jar:4.0.0.M3]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228) ~[spring-beans-4.0.0.M3.jar:4.0.0.M3]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:295) ~[spring-beans-4.0.0.M3.jar:4.0.0.M3]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-4.0.0.M3.jar:4.0.0.M3]
    at org.springframework.context.support.AbstractApplicationContext.registerBeanPostProcessors(AbstractApplicationContext.java:741) ~[spring-context-3.2.1.RELEASE.jar:3.2.1.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:464) ~[spring-context-3.2.1.RELEASE.jar:3.2.1.RELEASE]
    at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:384) ~[spring-web-3.1.0.RELEASE.jar:3.1.0.RELEASE]
    at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:283) ~[spring-web-3.1.0.RELEASE.jar:3.1.0.RELEASE]
    at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:111) ~[spring-web-3.1.0.RELEASE.jar:3.1.0.RELEASE]
    at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4791) ~[na:na]
    at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5285) ~[na:na]
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150) ~[na:na]
    at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:901) ~[na:na]
    at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:877) ~[na:na]
    at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:618) ~[na:na]
    at org.apache.catalina.startup.HostConfig.deployDescriptor(HostConfig.java:650) ~[na:7.0.29.A]
    at org.apache.catalina.startup.HostConfig$DeployDescriptor.run(HostConfig.java:1582) ~[na:7.0.29.A]
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471) ~[na:1.7.0_25]
    at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334) ~[na:1.7.0_25]
    at java.util.concurrent.FutureTask.run(FutureTask.java:166) ~[na:1.7.0_25]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) ~[na:1.7.0_25]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) ~[na:1.7.0_25]
    at java.lang.Thread.run(Thread.java:724) ~[na:1.7.0_25]

    ReplyDelete
  10. That is due to no spring context being set.

    Do a pull on the code in github and run again

    ReplyDelete
  11. Hi lain,
    this is an excellent series of articles about the Restful in java. I have a question: why you assigned multiple sessions to each user in the code, but as shown in the table diagram each user was associated with zero or one session. I am a little bit confused about this issue.

    Thanks

    ReplyDelete
  12. That was an error in the diagram. Thanks for pointing it out. I have updated it.

    ReplyDelete
    Replies
    1. Hi lain,
      Thanks for your reply. I have another three questions related to the session:
      (1) The first is why don't you just create one session token for each user and update the lastUpdated field of the session token instead of creating a new session token every time a user login. Based on the lastUpdated field, you can easily expire the session if it has not been updated for a period of time.
      (2) Besides, In the authorize method of the SessionTokenAuthorizationService class, you loop through all the session tokens that a user has, which is a little counterintuitive to me? I think each time a user has only one valid session, right? Could you please explain a little bit more on this to me?
      (3) the last question is how do you utilize the SessionReaper class to delete expired sessions? Is there any mechanism to automatically to do this?

      These might be rookie questions. But I really need help to clear these concepts up.

      very appreciated

      Delete
    2. Thanks for the code review.
      Initially I set up a User with multiple session tokens to support concurrent logins for the same account.

      In retrospect if I was to code it again I would implement something similar to OAuth2 and use the same token for all logins for that account with a useful expiration policy.
      If I get some time I'll probably rewrite it to do that.
      Having said that I have not used the approach in this code base for a couple of years.
      I use OAuth2

      As for the SessionReaper I use a useful utility in spring-task. if you look in root-context.xml you will see the bean defined:

      <bean id="sessionReaperCronJob" class="com.porterhead.rest.user.SessionReaper"/>
      <task:scheduled-tasks scheduler="sessionReaperScheduler">
      <task:scheduled ref="sessionReaperCronJob" method="cleanUpExpiredSessions" cron="0 0/1 * * * *"/>
      </task:scheduled-tasks>

      <task:scheduler id="sessionReaperScheduler" pool-size="1"/>

      Delete
    3. I have since replaced session tokens with a more OAuth-like AuthenticationToken approach.

      See the git commit for details.

      Delete
  13. I'm trying to modify the object returned on login, so in UserResource :

    private Response getLoginResponse(AuthenticatedUserToken token) {...}

    I've modified the AuthenticatedUserToken object but still, the response always just contains userID and sessionToken. Any idea ? is there anything that filters the response ?

    ReplyDelete
    Replies
    1. Figured it out. I had to put "getter" methods in AuthenticatedUserToken for the new members. (not sure how that magically works but oh well)

      Delete
  14. Hi. Thanks for these nice posts.
    I have a question : when signing up a user, a token is created and persisted with the user when we do "userRepository.save(newUser);".
    When login, we generate a new token, but I cannot see it being persisted. Why don't we? Am I missing something?
    Also on login, should we check if user isVerified too?

    Thanks

    ReplyDelete
    Replies
    1. Good catch. That was a bug. The user if now saved if a new token is issued.

      The verification check for a user is meant more as means for restricting access to site resources if the user is not verified.
      You would not want to prevent them logging in if they have not verified.

      Delete
    2. Thanks for such a quick response!

      Delete
  15. This contribution is awesome, thanks a lot for sharing this project.

    I didn't find the schema for importing the structure of the required MySQL tables (rest_user, rest_verification_token, etc.) at https://github.com/iainporter/rest-java. I would have expected to find in the schema folder along with the other *.sql files.

    I've got so far a Jersey + spring-data + Hibernate + MySQL REST project I want to protect. Would you recommend me to focus on your GitHub project "rest-java" or "oauth2-provider" ? Which one are you currently the most working with?

    ReplyDelete
    Replies
    1. The tables will be generated on startup

      I would recommend the OAuth2 approach to security now as it has become the standard.

      The oauth2-provider project uses MongoDB as the persistence repository for no other reason than I wanted to show use of something other than the usual hibernate/SQL approach.
      If you want to use SQL for the OAuth objects then Spring has some handy defaults you can integrate.

      Delete
    2. Could you please mentor us about these handy methods that we can apply to your oauth2-provider project quickly?
      Thank you

      Delete
    3. you can replace the custom mongo implementation of TokenStore with the Spring JDBC version

      in token-store.xml

      replace the token store bean reference :

      <bean id="tokenStore" class="com.porterhead.oauth2.mongodb.OAuth2RepositoryTokenStore">

      with:

      <bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.store.JdbcTokenStore">
      <constructor-arg ref="tokenStoreDatasource"/>
      </bean>

      Delete
  16. Mr Porter, Thank you for your response. When i applying your solution, occured an error. Have you got an idea about the issue?
    Brgds
    SEVERE: Context initialization failed
    org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.authentication.dao.DaoAuthenticationProvider#0': Cannot resolve reference to bean 'userService' while setting bean property 'userDetailsService'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userConfiguration': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private org.springframework.security.oauth2.provider.token.DefaultTokenServices com.trafficalarm.rest.configuration.UserConfiguration.tokenServices; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'tokenServices' defined in class path resource [spring/oauth/oauth2-configuration.xml]: Cannot resolve reference to bean 'tokenStore' while setting bean property 'tokenStore'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'tokenStore' defined in class path resource [spring/oauth/token-store.xml]: Cannot resolve reference to bean 'tokenStoreDatasource' while setting constructor argument; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'tokenStoreDatasource' is defined

    ReplyDelete
    Replies
    1. You have to provide this bean yourself:

      <constructor-arg ref="tokenStoreDatasource"/>

      in the usual Spring way of configuring a JDBC datasource

      If you need a script to create the schema you can find it here: https://github.com/spring-projects/spring-security-oauth/tree/master/spring-security-oauth2/src/test/resources

      Delete
  17. I try to apply your solution into my existing project. I stucked with the error that i shared below. And i can not forward. However, I am sure that all complaint classes are actually exists under right package. Seems to be that i am missing something.
    Could you plz lighten me? If you wanna see the project. Here it is=> https://github.com/webyildirim/trafikhaberci


    thx

    tomcat error log start such as:
    SEVERE: Context initialization failed
    org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#0': Invocation of init method failed; nested exception is org.springframework.beans.factory.CannotLoadBeanClassException: Cannot find class [com.trafficalarm.rest.security.HierarchicalJsr250Voter] for bean with name 'roleVoter' defined in class path resource [spring/security/security-configuration.xml]; nested exception is java.lang.ClassNotFoundException: com.trafficalarm.rest.security.HierarchicalJsr250Voter
    Related cause: org.springframework.beans.factory.CannotLoadBeanClassException: Cannot find class [com.trafficalarm.rest.security.HierarchicalJsr250Voter] for bean with name 'roleVoter' defined in class path resource [spring/security/security-configuration.xml]; nested exception is java.lang.ClassNotFoundException: com.trafficalarm.rest.security.HierarchicalJsr250Voter
    Related cause: org.springframework.beans.factory.CannotLoadBeanClassException: Cannot find class [com.trafficalarm.rest.filter.spring.SpringCrossOriginResourceSharingFilter] for bean with name 'corsFilter' defined in class path resource [spring/oauth/oauth2-configuration.xml]; nested exception is java.lang.ClassNotFoundException: com.trafficalarm.rest.filter.spring.SpringCrossOriginResourceSharingFilter
    Related cause: org.springframework.beans.factory.CannotLoadBeanClassException: Cannot find class [com.trafficalarm.rest.security.OAuthRestEntryPoint] for bean with name 'oauthRestEntryPoint' defined in class path resource [spring/oauth/oauth2-configuration.xml]; nested exception is java.lang.ClassNotFoundException: com.trafficalarm.rest.security.OAuthRestEntryPoint


    ReplyDelete