Posts on this topic:
Part 1: Create and run basic Grails Restful app
Part 3: Add PostgreSQL and start coding
Part 4: Secure end-points fully and cleanly
Part 5: Assure REST & Publish your API
Part 1: Create and run basic Grails Restful app
Part 3: Add PostgreSQL and start coding
Part 4: Secure end-points fully and cleanly
Part 5: Assure REST & Publish your API
Part 2: Add Core Security and REST API Security
In Part 1, I created a Grails 3 application using rest-api profile. In this part, I will list steps to add security. The Grails Spring Security Core plugin is one of the very popular Grails plugins and when Grails 3 was released, this was the most desired plugin by the Grails community to be migrated to Grails 3. Thanks to Burt Beckwith!Security
Typically, web application's resources (pages/urls) are secured through a login form. Spring Security framework is a very flexible framework and meets most of web application's security needs. Though it simplifies many aspects of security, it comes with some level of complexities.In Grails world, the popular Grails Spring Security Core plugin which underpins Spring Security makes security trivial. However, the core plugin is not just enough for securing RESTful Grails application. In a RESTful application, all end-points need to be secured and a stateless/session-less mechanism for safely accepting HTTP requests through HTTP methods to urls should be in place. The Grails Spring Security REST plugin which is written on top of Grails Spring Security Core plugin makes this possible in a simple manner. It supports token-based RESTful authentication. It also supports JSON Web Token (JWT).
Let's try some hands on...
Environment: Grails 3.1.6, Java 1.8, IntelliJ 15 on Mac OS X 10.9.5
Before I start adding Security, I will import the grails app created into my IntelliJ IDEA. IntelliJ 15 supports Grails 3. Also, Grails 3 switched it's build system from GANT to Gradle. So the Grails 3 app created can easily be imported as a Gradle project. IntelliJ recognizes it as a Grails project and adds all needed support.
To import into IntelliJ IDEA 15, simply follow these steps:
1. Open IntelliJ IDEA and go to File > New > Project from existing sources...
2. Select build.gradle file under the grails3 app created (in my example it's giri-api)
3. IntelliJ recognizes it as a Gradle project.
Go with default options in the next step.
If multiple JVMs are available in IntelliJ select the one you prefer for Gradle JVM.
I have 1.6, 1.7 and 1.8 and I selected 1.8. Press OK.
4. IntelliJ recognizes it as Gradle project, imports and builds the project.
It also recognizes the project dir as root module and prompts for modules. Just press OK.
5. It imports the project and prompts for Grails shell.
Click the link 'Select Grails SDK Home'.
I have multiple Grails versions installed and I select ~/.gvm/grails/3.1.6
6. If you miss step-5 due to any reason, you can add Grails SDK Home from:
File > Other Settings > Default Settings > Languages & Frameworks > Grails
Also, notice that IntelliJ adds a run configuration with the project name to run the main Application.groovy class. This lets you run the grails application from IntelliJ. Just click Run > Edit Configurations menu item or the drop down from the run configurations icon and take a look at the configuration. You can add more run configurations and setup various grails commands to run directly from IntelliJ.
Step 1 Add Security - Grails Spring security core plugin dependency to the application's build.gradle file
dependencies {
...
//grails security-core plugin
compile "org.grails.plugins:spring-security-core:3.1.0"
...
}
Step 2 Generate security related classes quickly
This step is documented well in the quick start section of the plugin documentation.
Minimally, we need User and Role domain classes for security. As domain classes have equivalent persistent db tables created by Grails, some databases wouldn't allow to have tables with names that are reserved words. At least, PostgresSQL will not let a table named User. So we I will use AppUser instead.
From the project home directory execute the following:
$ grails s2-quickstart com.giri.security AppUser Role
BUILD SUCCESSFUL
| Creating User class 'AppUser' and Role class 'Role' in package 'com.giri.security'
| Rendered template Person.groovy.template to destination grails-app/domain/com/giri/security/AppUser.groovy
| Rendered template Authority.groovy.template to destination grails-app/domain/com/giri/security/Role.groovy
| Rendered template PersonAuthority.groovy.template to destination grails-app/domain/com/giri/security/AppUserRole.groovy
|
************************************************************
* Created security-related domain classes. Your *
* grails-app/conf/application.groovy has been updated with *
* the class names of the configured domain classes; *
* please verify that the values are correct. *
************************************************************
The above command generates 3 classes: AppUser.groovy, Role.groovy and AppUserRoles.groovy under grails-app/domain/com/giri/security directory.
Also, take a look at application.groovy. It will have security plugin's default properties added. Some of these properties and values are obvious. We don't need to change anything at this time. Just make a note of grails.plugin.springsecurity.filterChain.chainMap, this needs some attention later when we secure rest end points using annotations.
// Added by the Spring Security Core plugin:
grails.plugin.springsecurity.userLookup.userDomainClassName = 'com.giri.security.AppUser'
grails.plugin.springsecurity.userLookup.authorityJoinClassName = 'com.giri.security.AppUserRole'
grails.plugin.springsecurity.authority.className = 'com.giri.security.Role'
grails.plugin.springsecurity.controllerAnnotations.staticRules = [
[pattern: '/', access: ['permitAll']],
[pattern: '/error', access: ['permitAll']],
[pattern: '/index', access: ['permitAll']],
[pattern: '/index.gsp', access: ['permitAll']],
[pattern: '/shutdown', access: ['permitAll']],
[pattern: '/assets/**', access: ['permitAll']],
[pattern: '/**/js/**', access: ['permitAll']],
[pattern: '/**/css/**', access: ['permitAll']],
[pattern: '/**/images/**', access: ['permitAll']],
[pattern: '/**/favicon.ico', access: ['permitAll']]
]
grails.plugin.springsecurity.filterChain.chainMap = [
[pattern: '/assets/**', filters: 'none'],
[pattern: '/**/js/**', filters: 'none'],
[pattern: '/**/css/**', filters: 'none'],
[pattern: '/**/images/**', filters: 'none'],
[pattern: '/**/favicon.ico', filters: 'none'],
[pattern: '/**', filters: 'JOINED_FILTERS']
]
Gotcha
When I ran s2-quickstart command for generating security domain classes, I passed package com.giri.security explicitly. Otherwise, grails uses the default package and the defaultPackage is defined in application.yml under elements grails: codegen: Out-of-the-box, grails uses application name as the default package with hiphens (-) converted to dots (.), if hiphens are used in the application name. My application name is giri-api and hence when I created the app, the defaultPackage was set to giri.api in application.yml. You can change it to the default package you would like to have. Once changed, any grails artifacts generated using grails create-xxxxx commands use this as the default package and generate all classes under that package. I changed the default to com.giri for convenience.
Step 3 Run the app
$ grails run-app
Configuring Spring Security Core ...
... finished configuring Spring Security Core
Grails application running at http://localhost:8080 in environment: development
Point your browser at: http://localhost:8080
It will return {"message":"Internal server error","error":500}
Check the console, you will see:
ERROR org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/].[grailsDispatcherServlet] - Servlet.service() for servlet [grailsDispatcherServlet] in context with path [] threw exception [Could not resolve view with name '/login/auth' in servlet with name 'grailsDispatcherServlet'] with root cause
javax.servlet.ServletException: Could not resolve view with name '/login/auth' in servlet with name 'grailsDispatcherServlet'
This is because, now the application is secured and as the security plugin added a chain of filters, the request is redirected to a login page. we do not have a login page yet. We do not need one anyway as this is a RESTful API application.
Step 4 Add roles and users
As outlined in the security plugin documentation in Bootstrap.groovy, add the following code:
import com.giri.security.AppUser
import com.giri.security.AppUserRole
import com.giri.security.Role
class BootStrap {
def init = { servletContext ->
def init = {
def adminRole = new Role('ROLE_ADMIN').save()
def userRole = new Role('ROLE_USER').save()
def testUser = new AppUser('me', 'password').save()
def adminUser = new AppUser('admin', 'admin').save()
AppUserRole.create testUser, userRole
AppUserRole.create adminUser, adminRole
AppUserRole.withSession {
it.flush()
it.clear()
}
assert AppUser.count() == 2
assert Role.count() == 2
assert AppUserRole.count() == 2
}
}
def destroy = {
}
}
Tip
You can even make the above code to create roles and users better with GORM's findOrSaveByXxxx() methods.
def adminRole = Role.findOrSaveByAuthority('ROLE_ADMIN')
def userRole = Role.findOrSaveByAuthority('ROLE_USER')
def testUser = AppUser.findOrSaveByUsernameAndPassword('me', 'password')
def adminUser = AppUser.findOrSaveByUsernameAndPassword('admin', 'admin')
Step 5 Start the application
The application results with the following exception:
ERROR org.springframework.boot.SpringApplication - Application startup failed
groovy.lang.GroovyRuntimeException: Could not find matching constructor for: com.giri.security.Role(java.lang.String)
Fix
Add the following missing constructors for AppUser.groovy and Role.groovy
AppUser(String username, String password) {
this()
this.username = username
this.password = password
}
Role(String authority) {
this()
this.authority = authority
}
Step 6 Add REST Security API plugin with GORM support
Add REST API Security plugin dependency to build.gradle file
ext{
springSecurityRestVersion = '2.0.0.M2'
}
dependencies {
//grails REST API Security plugin
compile "org.grails.plugins:spring-security-rest:${springSecurityRestVersion}"
compile "org.grails.plugins:spring-security-rest-gorm:${springSecurityRestVersion}"
}
Step 7 Restart the application
$ grails run-app
Configuring Spring Security Core ...
... finished configuring Spring Security Core
Configuring Spring Security REST 2.0.0.M2...
... finished configuring Spring Security REST
... with GORM support
Grails application running at http://localhost:8080 in environment: development
Notice the highlighted.Step 8 Add plugin chainMap property in application.groovy file as described in the documentation
Add the following as outlined in the plugin documentation:
grails.plugin.springsecurity.filterChain.chainMap = [
//Stateless chain
[ pattern: '/api/**', filters: 'JOINED_FILTERS,-anonymousAuthenticationFilter,-exceptionTranslationFilter,-authenticationProcessingFilter,-securityContextPersistenceFilter,-rememberMeAuthenticationFilter'],
//Traditional chain
//[ pattern: '/**', filters: 'JOINED_FILTERS,-restTokenValidationFilter,-restExceptionTranslationFilter']
]
Step 9 Generate domain class AuthenticationToken as described in the plugin documentation
$ grails create-domain-class com.giri.security.AuthenticationToken
| Created grails-app/domain/com/giri/security/AuthenticationToken.groovy
| Created src/test/groovy/com/giri/security/AuthenticationTokenSpec.groovy
Step 10 Update AuthenticationToken.groovy
Open file AuthenticationToken.groovy and update as shown below:
class AuthenticationToken {
String tokenValue
String username
static mapping = {
version false
}
static constraints = {
}
}
Step 11 Open application.groovy and add the following plugin related properties:
grails.plugin.springsecurity.rest.token.storage.useGorm = true
grails.plugin.springsecurity.rest.token.storage.gorm.tokenDomainClassName = 'com.giri.security.AuthenticationToken'
Step 12 Restart the application and point your browser at: http://localhost:8080
Now, you will see the default JSON response:
Grails comes with database console for the in-memory database H2. By default when the application is run, it runs in development environment and H2 in-memory database is used.
Point your browser at http://localhost:8080/dbconsole to see the in-memory H2 database console. Select Generic H2 Embedded and click connect. Notice that you will have 4 tables: APP_USER, ROLE, APP_USER_ROLE and AUTHENTICATION_TOKEN
Just click connect and notice all the domain classes added now have corresponding tables created:
Step 14 Test it
The Grails Security REST API plugin provides three end-points: /api/login for login, /api/logout for logout and /api/validate for validating the token.
Let's test it little bit. Since it is a RESTful application, we do not have regular gsp pages/views. We need to send RESTful requests to test our application.
Let's use Curl - a simple command line tool that comes with Mac and is also available for Windows.
Login - Send a POST request with right user credentials
Run the following command to test end-point /api/login. Highlighted is the response received.
$ curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X POST -d '{"username":"me","password":"password"}' http://localhost:8080/api/login
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Cache-Control: no-store
Pragma: no-cache
Content-Type: application/json;charset=UTF-8
Content-Length: 112
Date: Sun, 19 Jun 2016 15:12:48 GMT
{"username":"me","roles":["ROLE_USER"],"token_type":"Bearer","access_token":"664dkbafcuo4prsd02vocvlfvaok5nvl"}
Login - Send a POST request with wrong user credentials
Run the following command to test end-point /api/login. Highlighted is the response received.
$ curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X POST -d '{"username":"me","password":"passwordWrong"}' http://localhost:8080/api/login
HTTP/1.1 401 Unauthorized
Server: Apache-Coyote/1.1
WWW-Authenticate: Bearer
Content-Length: 0
Date: Sun, 19 Jun 2016 15:19:02 GMT
Notice the Unauthorized response
The plugin also has an endpoint to validate the token received upon successful login.
Validate token
Run the following command to test end-point /api/validate. Highlighted is the response received.
$ curl -i -H "Authorization: Bearer 664dkbafcuo4prsd02vocvlfvaok5nvl" http://localhost:8080/api/validate
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Cache-Control: no-store
Pragma: no-cache
Content-Type: application/json;charset=UTF-8
Content-Length: 112
Date: Tue, 21 Jun 2016 01:03:05 GMT
{"username":"me","roles":["ROLE_USER"],"token_type":"Bearer","access_token":"664dkbafcuo4prsd02vocvlfvaok5nvl"}
Highlighted is the response.
NOTE
"Authorization: Bearer <token>" is the default header to be passed.
Set the following in application.grovy to change the header.
grails.plugin.springsecurity.rest.token.validation.useBearerToken = false
grails.plugin.springsecurity.rest.token.validation.headerName = 'X-Auth-Token'
If you change the header name as above, your request to login should be like:
$ curl -i -H "X-Auth-Token: 664dkbafcuo4prsd02vocvlfvaok5nvl" http://localhost:8080/api/validate
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Cache-Control: no-store
Pragma: no-cache
Content-Type: application/json;charset=UTF-8
Content-Length: 90
Date: Tue, 21 Jun 2016 01:20:36 GMT
{"username":"me","roles":["ROLE_USER"],"access_token":"664dkbafcuo4prsd02vocvlfvaok5nvl"}
Highlighted is the response
Validate wrong token Run the following. Highlighted is the response
$ curl -i -H "X-Auth-Token: wrong-token-anyway" http://localhost:8080/api/validate
HTTP/1.1 401 Unauthorized
Server: Apache-Coyote/1.1
Content-Length: 0
Date: Tue, 21 Jun 2016 01:23:38 GMT
Logout Let's test the logout end-point: /api/logout. Run the following and highlighted is the response.
$ curl -i -H "X-Auth-Token: 664dkbafcuo4prsd02vocvlfvaok5nvl" http://localhost:8080/api/logout
HTTP/1.1 403 Forbidden
Server: Apache-Coyote/1.1
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 21 Jun 2016 01:26:40 GMT
{"timestamp":1466472400548,"status":403,"error":"Forbidden","message":"Access is denied","path":"/api/logout"}
Surprisingly we received 403 Forbidden.
Gotcha
For logout to work, add the following pattern to the list of static rules in application.groovy
grails.plugin.springsecurity.controllerAnnotations.staticRules = [
//omitted the rest for brevity
[pattern: '/api/logout', access: ['isAuthenticated()']
]
By adding the above pattern to staticRules, we are specifying to allow the end-point api/logout only if the user is authenticated. Now try to logout.
curl -i -H "X-Auth-Token: 664dkbafcuo4prsd02vocvlfvaok5nvl" http://localhost:8080/api/logout
HTTP/1.1 405 Method Not Allowed
Server: Apache-Coyote/1.1
X-Application-Context: application:development
Content-Length: 0
Date: Tue, 21 Jun 2016 01:34:48 GMT
Highlighted is the response. This time it's 405 Method not allowed.
The reason: We used a GET request to logout. The security plugin has a property grails.plugin.springsecurity.logout.postOnly which is set to true by default. So, it only allows POST request to logout.
Try the following now
$ curl -i -H "X-Auth-Token:664dkbafcuo4prsd02vocvlfvaok5nvl" -X POST http://localhost:8080/api/logout HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
X-Application-Context: application:development
Content-Length: 0
Date: Tue, 21 Jun 2016 01:36:29 GMT
Highlighted is the response. Now it successfully logged me out.
Now double check by running the validate end-point
$ curl -i -H "X-Auth-Token:664dkbafcuo4prsd02vocvlfvaok5nvl" http://localhost:8080/api/validate
HTTP/1.1 401 Unauthorized
Server: Apache-Coyote/1.1
Content-Length: 0
Date: Tue, 21 Jun 2016 01:49:02 GMT
Highlighted is the response. Notice that the token that was valid now resulted with 401 Unauthorized.
TIP
To make the logout work with GET request, set grails.plugin.springsecurity.logout.postOnly = false in application.groovy. The GET request to logout should work.
TIP
If you are not comfortable with the command line tool like curl for testing, thee are several graphical tools available. I use this chrome plugin called DTC REST Client from Restlet which is really nice, easy to use and also all API tests can be organized, saved and shared with your team.
Step 15 Lockdown Spring Boot Actuator end-points
Grails 3 underpins Spring Boot. Spring Boot has a module called Actuator which provides various end-points for monitoring and managing a running application. In Part-1 side note, I mentioned about it. These end-points are available at root the application context path.
The stateless chainMap pattern that we added to secure all our /api/** end-points won't secure Actuator end points. In reality, it makes sense to secure Actuator provided application monitoring and management end-points. There could be many ways to secure these end-points. Here is what I tried which worked.
- Customize the Actuator end-points path by adding the following spring property in application.yml
- With the above spring property we changed end-points path from application root context to '/api/manamgement'. With this change for example the earlier http://localhost:8080/info will be available at http://localhost:8080/api/management/info. Now, Add the following static rule in application.groovy to secure all Actuator end-points.
management:
context-path: /api/management
grails.plugin.springsecurity.controllerAnnotations.staticRules = [
...
[pattern: '/api/management/**', access:['isAuthenticated()']]
]
[pattern: '/api/management/**', access:['ROLE_ADMIN']]
Now we have successfully added security to our app in RESTful way with the two Grails plugins. In the next post, we will add PostgreSQL support to the application.
References
Documentations
Grails Spring Core Plugin Documentation
Grails Spring Security REST Plugin Documentation
Spring Security Documentation
Grails Goodness: Adding Health Check Indicators
Code/APIs
Grails Spring Security Core Plugin code
Grails Spring Security REST Plugin code
Spring Security Code
Spring Security API
Excellent! Thanks so much.
ReplyDeleteOn Step 12 & 13, I am getting a [Could not resolve view with name '/login/auth'] error when trying to load the http://localhost:8080 from the browser. But I am able to run Step 14 without an issue.
ReplyDeleteThank you very much for sharing!
Not sure why without much details. Sorry about the late reply.
DeleteYou got the error about not finding a matching constructor for Role because you did not call it properly!!!
ReplyDeleteInstead of
def adminRole = new Role('ROLE_ADMIN').save()
You should have used
def adminRole = new Role(authority: 'ROLE_ADMIN').save()
The argument has to be named!
The same goes for the problem with the "AppUser". Make sure to name your arguments:
...User(username: 'me', password: 'password')
Heads up everybody :)
Thanks for the update. I am sorry I missed to read your comment...
DeleteThank you very much. But I am confused which one to select either JWT or GORM. Could you please guide me which one is better for performance and security
ReplyDeleteIt depends actually, I wouldn't worry about performance when it comes to security. Performance should not be a factor that drives choosing one over the other...
ReplyDeleteGreat job! Very helpful for my project.
ReplyDeleteYou explained everything very well.
Thanks very much.
I am happy to know that it was helpful. Thanks for sharing.
Delete