├── .gitignore ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── grails-app ├── assets │ └── stylesheets │ │ └── errors.css ├── conf │ ├── application.yml │ ├── logback.groovy │ └── spring │ │ └── resources.groovy ├── controllers │ ├── UrlMappings.groovy │ └── demo │ │ ├── ProfileController.groovy │ │ └── RegisterController.groovy ├── domain │ └── demo │ │ ├── Person.groovy │ │ ├── PersonSecurityRole.groovy │ │ └── SecurityRole.groovy ├── i18n │ └── messages.properties ├── init │ ├── BootStrap.groovy │ └── demo │ │ ├── Application.groovy │ │ ├── OAuth2ServerConfiguration.groovy │ │ └── WebSecurityConfiguration.groovy ├── migrations │ ├── changelog-security.groovy │ └── changelog.groovy ├── services │ └── demo │ │ └── CustomUserDetailsService.groovy └── views │ ├── error.gsp │ ├── index.gsp │ └── notFound.gsp ├── sql └── setup.sql └── src ├── integration-test └── groovy │ └── demo │ ├── AuthenticatedSpec.groovy │ ├── ProfileSpec.groovy │ ├── RegisterSpec.groovy │ └── UnauthenticatedSpec.groovy └── main └── groovy └── demo └── marshallers └── PersonMarshallerJson.groovy /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .gradle 3 | /build 4 | /out 5 | /*.iml 6 | /*.ipr 7 | /*.iws 8 | *.classpath 9 | *.project 10 | *.settings 11 | /bin 12 | /subprojects/*/bin 13 | .DS_Store 14 | /performanceTest/lib 15 | .textmate 16 | /incoming-distributions 17 | .idea 18 | .idea_modules 19 | *.sublime-* 20 | .nb-gradle 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Demo API 2 | 3 | This Demo API is meant to serve as a golden sample for Grails 3 with OAuth 2. If 4 | you have any ideas for improvements or additions, please feel free to fork the 5 | repo and create a pull request! 6 | 7 | There is a screencast that walks through the codebase and explains how everything 8 | is setup. You can view it here: [http://blog.agileorbit.com/2015/05/11/Grails3-OAuth2.html](http://blog.agileorbit.com/2015/05/11/Grails3-OAuth2.html) 9 | 10 | ## Run 11 | 12 | Use Gradle: 13 | 14 | ```sh 15 | ./gradlew bootRun 16 | ``` 17 | 18 | ## Test 19 | 20 | Run functional tests: 21 | 22 | ```sh 23 | ./gradlew integrationTest 24 | ``` 25 | 26 | 27 | ## Usage 28 | 29 | Test the `profile` endpoint: 30 | 31 | ```sh 32 | curl http://localhost:8080/profile 33 | ``` 34 | 35 | You should receive the following JSON response, which indicates you are not authorized to access the resource: 36 | 37 | ```json 38 | { 39 | "error": "unauthorized", 40 | "error_description": "Full authentication is required to access this resource" 41 | } 42 | ``` 43 | 44 | In order to access the protected resource, you must first request an access token via the OAuth handshake. The API supports both `password` and `client_credentials` OAuth grant types. 45 | The `password` grant type should be used for API calls that require a particular user. The `client_credentials` grant type should be used for API calls when there is no user (i.e. registration). 46 | The entire API is secured by OAuth with the exception of the token endpoint `/oauth/token` which is secured by basic authentication. 47 | 48 | ### Client Credentials Grant Type (non-user API calls) 49 | 50 | Request an OAuth authorization with `client_credentials` grant type using the `client_id` and `client_secret` for basic authentication: 51 | 52 | ```sh 53 | curl -X POST -u demo-client:123456 http://localhost:8080/oauth/token\?grant_type=client_credentials -H "Accept: application/json" 54 | ``` 55 | A successful `client_credentials` authorization results in the following JSON response: 56 | 57 | ```json 58 | { 59 | "access_token": "cf96e458-12a1-4854-908d-419a35118363", 60 | "token_type": "bearer", 61 | "expires_in": 43199, 62 | "scope": "read write" 63 | } 64 | ``` 65 | 66 | Then use the `access_token` to call the `/register` endpoint to create a new user account: 67 | 68 | ```sh 69 | curl http://localhost:8080/register -H "Authorization: Bearer cf96e458-12a1-4854-908d-419a35118363" -H "Content-Type: application/json" -X POST -d '{"username":"bobbywarner", "password":"xyz", "email":"bobbywarner@gmail.com", "fullName": "Bobby Warner"}' 70 | ``` 71 | 72 | A successful registration results in the following JSON response (the password is not returned): 73 | 74 | ```json 75 | { 76 | "fullName": "Bobby Warner", 77 | "email": "bobbywarner@gmail.com", 78 | "username": "bobbywarner" 79 | } 80 | ``` 81 | ### Password Grant Type (user-specific API calls) 82 | 83 | Request an OAuth authorization with `password` grant type using the `client_id` and `client_secret` for basic authentication as well as the username and password for the specific user: 84 | 85 | ```sh 86 | curl -X POST -u demo-client:123456 http://localhost:8080/oauth/token -H "Accept: application/json" -d "password=xyz&username=bobbywarner&grant_type=password&scope=read%20write" 87 | ``` 88 | 89 | A successful `password` authorization results in the following JSON response. This is very similar to the `client_credentials` authorization except it also includes a `refresh_token`. 90 | 91 | ```json 92 | { 93 | "access_token": "ff16372e-38a7-4e29-88c2-1fb92897f558", 94 | "token_type": "bearer", 95 | "refresh_token": "f554d386-0b0a-461b-bdb2-292831cecd57", 96 | "expires_in": 43199, 97 | "scope": "read write" 98 | } 99 | ``` 100 | 101 | Then use the `access_token` returned in the previous request to make authorized requests to any additional protected endpoints: 102 | 103 | ```sh 104 | curl http://localhost:8080/profile -H "Authorization: Bearer ff16372e-38a7-4e29-88c2-1fb92897f558" 105 | ``` 106 | 107 | ### Refresh Token Grant Type 108 | 109 | After the specified time period, the `access_token` will expire. Use the `refresh_token` that was returned in the `password` OAuth authorization to retrieve a new `access_token`: 110 | 111 | ```sh 112 | curl -X POST -u demo-client:123456 http://localhost:8080/oauth/token -H "Accept: application/json" -d "grant_type=refresh_token&refresh_token=f554d386-0b0a-461b-bdb2-292831cecd57" 113 | ``` 114 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | grailsVersion = project.grailsVersion 4 | } 5 | repositories { 6 | mavenLocal() 7 | maven { url "https://repo.grails.org/grails/core" } 8 | } 9 | dependencies { 10 | classpath "org.grails:grails-gradle-plugin:$grailsVersion" 11 | classpath 'com.bertramlabs.plugins:asset-pipeline-gradle:2.1.1' 12 | classpath 'org.grails.plugins:database-migration:2.0.0.RC1' 13 | } 14 | } 15 | 16 | plugins { 17 | id "io.spring.dependency-management" version "0.4.0.RELEASE" 18 | } 19 | 20 | version "0.1.0" 21 | group "demo" 22 | 23 | apply plugin: "spring-boot" 24 | apply plugin: "war" 25 | apply plugin: "asset-pipeline" 26 | apply plugin: 'eclipse' 27 | apply plugin: 'idea' 28 | apply plugin: "org.grails.grails-web" 29 | apply plugin: "org.grails.grails-gsp" 30 | 31 | ext { 32 | grailsVersion = project.grailsVersion 33 | gradleWrapperVersion = project.gradleWrapperVersion 34 | } 35 | 36 | assets { 37 | minifyJs = true 38 | minifyCss = true 39 | } 40 | 41 | repositories { 42 | mavenLocal() 43 | maven { url "https://repo.grails.org/grails/core" } 44 | } 45 | 46 | dependencyManagement { 47 | imports { 48 | mavenBom "org.grails:grails-bom:$grailsVersion" 49 | } 50 | applyMavenExclusions false 51 | } 52 | 53 | dependencies { 54 | compile "org.springframework.boot:spring-boot-starter-security" 55 | compile "org.springframework.boot:spring-boot-starter-logging" 56 | compile "org.springframework.boot:spring-boot-starter-actuator" 57 | compile "org.springframework.boot:spring-boot-autoconfigure" 58 | compile "org.springframework.boot:spring-boot-starter-tomcat" 59 | compile "org.grails:grails-dependencies" 60 | compile "org.grails:grails-web-boot" 61 | 62 | compile "org.grails.plugins:hibernate" 63 | compile "org.grails.plugins:cache" 64 | compile "org.hibernate:hibernate-ehcache" 65 | 66 | runtime "org.grails.plugins:asset-pipeline" 67 | runtime "org.grails.plugins:scaffolding" 68 | 69 | testCompile "org.grails:grails-plugin-testing" 70 | testCompile "org.grails.plugins:geb" 71 | 72 | // Note: It is recommended to update to a more robust driver (Chrome, Firefox etc.) 73 | testRuntime 'org.seleniumhq.selenium:selenium-htmlunit-driver:2.44.0' 74 | 75 | console "org.grails:grails-console" 76 | 77 | compile "org.springframework.security.oauth:spring-security-oauth2:2.0.7.RELEASE" 78 | 79 | runtime "org.postgresql:postgresql:9.3-1101-jdbc41" 80 | runtime 'org.grails.plugins:database-migration:2.0.0.RC1' 81 | 82 | compile('org.grails:grails-datastore-rest-client:4.0.0.RELEASE') { 83 | exclude group: 'javax.servlet', module: 'javax.servlet-api' 84 | exclude group: 'commons-codec', module: 'commons-codec' 85 | exclude group: 'org.grails', module: 'grails-plugin-converters' 86 | exclude group: 'org.grails', module: 'grails-core' 87 | exclude group: 'org.grails', module: 'grails-web' 88 | } 89 | } 90 | 91 | task wrapper(type: Wrapper) { 92 | gradleVersion = gradleWrapperVersion 93 | } 94 | 95 | sourceSets { 96 | main { 97 | resources { 98 | srcDir 'grails-app/migrations' 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | grailsVersion=3.0.1 2 | gradleWrapperVersion=2.3 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobbywarner/grails3-oauth2-api/f25828eece2d120891d46e2dcc5bc860b661af5e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Apr 25 08:08:01 CDT 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /grails-app/assets/stylesheets/errors.css: -------------------------------------------------------------------------------- 1 | h1, h2 { 2 | margin: 10px 25px 5px; 3 | } 4 | 5 | h2 { 6 | font-size: 1.1em; 7 | } 8 | 9 | .filename { 10 | font-style: italic; 11 | } 12 | 13 | .exceptionMessage { 14 | margin: 10px; 15 | border: 1px solid #000; 16 | padding: 5px; 17 | background-color: #E9E9E9; 18 | } 19 | 20 | .stack, 21 | .snippet { 22 | margin: 0 25px 10px; 23 | } 24 | 25 | .stack, 26 | .snippet { 27 | border: 1px solid #ccc; 28 | -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2); 29 | -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2); 30 | box-shadow: 0 0 2px rgba(0,0,0,0.2); 31 | } 32 | 33 | /* error details */ 34 | .error-details { 35 | border-top: 1px solid #FFAAAA; 36 | -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2); 37 | -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2); 38 | box-shadow: 0 0 2px rgba(0,0,0,0.2); 39 | border-bottom: 1px solid #FFAAAA; 40 | -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2); 41 | -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2); 42 | box-shadow: 0 0 2px rgba(0,0,0,0.2); 43 | background-color:#FFF3F3; 44 | line-height: 1.5; 45 | overflow: hidden; 46 | padding: 5px; 47 | padding-left:25px; 48 | } 49 | 50 | .error-details dt { 51 | clear: left; 52 | float: left; 53 | font-weight: bold; 54 | margin-right: 5px; 55 | } 56 | 57 | .error-details dt:after { 58 | content: ":"; 59 | } 60 | 61 | .error-details dd { 62 | display: block; 63 | } 64 | 65 | /* stack trace */ 66 | .stack { 67 | padding: 5px; 68 | overflow: auto; 69 | height: 150px; 70 | } 71 | 72 | /* code snippet */ 73 | .snippet { 74 | background-color: #fff; 75 | font-family: monospace; 76 | } 77 | 78 | .snippet .line { 79 | display: block; 80 | } 81 | 82 | .snippet .lineNumber { 83 | background-color: #ddd; 84 | color: #999; 85 | display: inline-block; 86 | margin-right: 5px; 87 | padding: 0 3px; 88 | text-align: right; 89 | width: 3em; 90 | } 91 | 92 | .snippet .error { 93 | background-color: #fff3f3; 94 | font-weight: bold; 95 | } 96 | 97 | .snippet .error .lineNumber { 98 | background-color: #faa; 99 | color: #333; 100 | font-weight: bold; 101 | } 102 | 103 | .snippet .line:first-child .lineNumber { 104 | padding-top: 5px; 105 | } 106 | 107 | .snippet .line:last-child .lineNumber { 108 | padding-bottom: 5px; 109 | } -------------------------------------------------------------------------------- /grails-app/conf/application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | grails: 3 | profile: web 4 | codegen: 5 | defaultPackage: demo 6 | info: 7 | app: 8 | name: '@info.app.name@' 9 | version: '@info.app.version@' 10 | grailsVersion: '@info.app.grailsVersion@' 11 | spring: 12 | groovy: 13 | template: 14 | check-template-location: false 15 | 16 | --- 17 | grails: 18 | mime: 19 | disable: 20 | accept: 21 | header: 22 | userAgents: 23 | - Gecko 24 | - WebKit 25 | - Presto 26 | - Trident 27 | types: 28 | all: '*/*' 29 | atom: application/atom+xml 30 | css: text/css 31 | csv: text/csv 32 | form: application/x-www-form-urlencoded 33 | html: 34 | - text/html 35 | - application/xhtml+xml 36 | js: text/javascript 37 | json: 38 | - application/json 39 | - text/json 40 | multipartForm: multipart/form-data 41 | rss: application/rss+xml 42 | text: text/plain 43 | hal: 44 | - application/hal+json 45 | - application/hal+xml 46 | xml: 47 | - text/xml 48 | - application/xml 49 | urlmapping: 50 | cache: 51 | maxsize: 1000 52 | controllers: 53 | defaultScope: singleton 54 | converters: 55 | encoding: UTF-8 56 | views: 57 | default: 58 | codec: html 59 | gsp: 60 | encoding: UTF-8 61 | htmlcodec: xml 62 | codecs: 63 | expression: html 64 | scriptlets: html 65 | taglib: none 66 | staticparts: none 67 | hibernate: 68 | cache: 69 | queries: false 70 | region: 71 | factory_class: org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory 72 | --- 73 | dataSource: 74 | pooled: true 75 | jmxExport: true 76 | driverClassName: org.postgresql.Driver 77 | 78 | environments: 79 | development: 80 | grails: 81 | serverURL: http://localhost:8080 82 | dataSource: 83 | driverClassName: org.postgresql.Driver 84 | dialect: org.hibernate.dialect.PostgreSQL9Dialect 85 | dbCreate: none 86 | url: jdbc:postgresql://localhost:5432/demo_dev 87 | username: demo_dev 88 | password: 89 | test: 90 | grails: 91 | serverURL: http://localhost:8080 92 | dataSource: 93 | driverClassName: org.postgresql.Driver 94 | dialect: org.hibernate.dialect.PostgreSQL9Dialect 95 | dbCreate: none 96 | url: jdbc:postgresql://localhost:5432/demo_test 97 | username: demo_test 98 | password: 99 | production: 100 | grails: 101 | serverURL: http://localhost:8080 102 | dataSource: 103 | driverClassName: org.postgresql.Driver 104 | dialect: org.hibernate.dialect.PostgreSQL9Dialect 105 | dbCreate: none 106 | url: jdbc:postgresql://localhost:5432/demo_prod 107 | username: demo_prod 108 | password: 109 | properties: 110 | jmxEnabled: true 111 | initialSize: 5 112 | maxActive: 50 113 | minIdle: 5 114 | maxIdle: 25 115 | maxWait: 10000 116 | maxAge: 600000 117 | timeBetweenEvictionRunsMillis: 5000 118 | minEvictableIdleTimeMillis: 60000 119 | validationQuery: SELECT 1 120 | validationQueryTimeout: 3 121 | validationInterval: 15000 122 | testOnBorrow: true 123 | testWhileIdle: true 124 | testOnReturn: false 125 | jdbcInterceptors: ConnectionState 126 | defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED 127 | --- 128 | grails: 129 | plugin: 130 | databasemigration: 131 | updateOnStart: true 132 | -------------------------------------------------------------------------------- /grails-app/conf/logback.groovy: -------------------------------------------------------------------------------- 1 | import grails.util.BuildSettings 2 | import grails.util.Environment 3 | 4 | 5 | // See http://logback.qos.ch/manual/groovy.html for details on configuration 6 | appender('STDOUT', ConsoleAppender) { 7 | encoder(PatternLayoutEncoder) { 8 | pattern = "%level %logger - %msg%n" 9 | } 10 | } 11 | 12 | root(ERROR, ['STDOUT']) 13 | 14 | if(Environment.current == Environment.DEVELOPMENT) { 15 | def targetDir = BuildSettings.TARGET_DIR 16 | if(targetDir) { 17 | 18 | appender("FULL_STACKTRACE", FileAppender) { 19 | 20 | file = "${targetDir}/stacktrace.log" 21 | append = true 22 | encoder(PatternLayoutEncoder) { 23 | pattern = "%level %logger - %msg%n" 24 | } 25 | } 26 | logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /grails-app/conf/spring/resources.groovy: -------------------------------------------------------------------------------- 1 | import demo.marshallers.* 2 | import grails.converters.JSON 3 | import org.grails.web.converters.configuration.ObjectMarshallerRegisterer 4 | 5 | beans = { 6 | personJsonMarshaller(ObjectMarshallerRegisterer) { 7 | marshaller = new PersonMarshallerJson() 8 | converterClass = JSON 9 | priority = 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /grails-app/controllers/UrlMappings.groovy: -------------------------------------------------------------------------------- 1 | class UrlMappings { 2 | 3 | static mappings = { 4 | 5 | '/register'(controller: 'register') { 6 | action = [POST: 'save'] 7 | format = 'json' 8 | } 9 | 10 | '/profile'(controller: 'profile') { 11 | action = [GET: 'index'] 12 | } 13 | 14 | "/"(view:"/index") 15 | "500"(view:'/error') 16 | "404"(view:'/notFound') 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /grails-app/controllers/demo/ProfileController.groovy: -------------------------------------------------------------------------------- 1 | package demo 2 | 3 | import org.springframework.security.core.context.SecurityContextHolder 4 | 5 | import javax.annotation.security.RolesAllowed 6 | 7 | @RolesAllowed(["ROLE_CLIENT"]) 8 | class ProfileController { 9 | 10 | def index() { 11 | render SecurityContextHolder.context?.authentication?.principal 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /grails-app/controllers/demo/RegisterController.groovy: -------------------------------------------------------------------------------- 1 | package demo 2 | 3 | import grails.converters.JSON 4 | import grails.transaction.Transactional 5 | 6 | import static org.springframework.http.HttpStatus.* 7 | 8 | class RegisterController { 9 | 10 | static allowedMethods = [save: "POST"] 11 | 12 | @Transactional 13 | def save(RegisterCommand signup) { 14 | log.debug("Content-Type: " + request.getHeader("Content-Type")) 15 | log.debug("Accept: " + request.getHeader("Accept")) 16 | if (!signup.hasErrors()) { 17 | Person person = Person.findByUsername(signup.username) 18 | if (!person) { 19 | person = Person.findByEmail(signup.email) 20 | if (!person) { 21 | person = new Person(username: signup.username, password: signup.password, fullName: signup.fullName, email: signup.email).save(flush: true) 22 | def userRole = SecurityRole.findByAuthority('ROLE_CLIENT') 23 | PersonSecurityRole.create(person, userRole) 24 | } else { 25 | response.status = 400 26 | def error = [ 27 | [ 28 | field: 'email', 29 | 'rejected-value': signup.email, 30 | message: 'A user already exists with this email address' 31 | ] 32 | ] 33 | def results = [errors: error] 34 | render results as JSON 35 | return 36 | } 37 | } else { 38 | response.status = 400 39 | def error = [ 40 | [ 41 | field: 'username', 42 | 'rejected-value': signup.username, 43 | message: 'A user already exists with this username' 44 | ] 45 | ] 46 | def results = [errors: error] 47 | render results as JSON 48 | return 49 | } 50 | respond person, [status: CREATED] 51 | } else { 52 | response.status = 400 53 | render signup.errors as JSON 54 | } 55 | } 56 | } 57 | 58 | class RegisterCommand { 59 | String fullName 60 | String username 61 | String email 62 | String password 63 | 64 | static constraints = { 65 | username(blank: false, unique: true) 66 | email(blank: false, unique: true, email: true) 67 | password(blank: false) 68 | fullName(blank: false) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /grails-app/domain/demo/Person.groovy: -------------------------------------------------------------------------------- 1 | package demo 2 | 3 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder 4 | import org.springframework.security.crypto.password.PasswordEncoder 5 | 6 | class Person { 7 | 8 | String username 9 | String email 10 | String password 11 | String fullName 12 | 13 | static constraints = { 14 | username(blank: false, unique: true) 15 | email(blank: false, unique: true, email: true) 16 | password(blank: false) 17 | fullName(blank: false) 18 | } 19 | 20 | Set getSecurityRoles() { 21 | PersonSecurityRole.findAllByPerson(this).collect { it.securityRole } 22 | } 23 | 24 | def beforeInsert() { 25 | encodePassword() 26 | } 27 | 28 | def beforeUpdate() { 29 | if (isDirty('password')) { 30 | encodePassword() 31 | } 32 | } 33 | 34 | protected void encodePassword() { 35 | PasswordEncoder passwordEncoder = new BCryptPasswordEncoder() 36 | password = passwordEncoder.encode(password) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /grails-app/domain/demo/PersonSecurityRole.groovy: -------------------------------------------------------------------------------- 1 | package demo 2 | 3 | import org.apache.commons.lang.builder.HashCodeBuilder 4 | 5 | class PersonSecurityRole implements Serializable { 6 | 7 | private static final long serialVersionUID = 1L 8 | 9 | Person person 10 | SecurityRole securityRole 11 | 12 | boolean equals(other) { 13 | if (!(other instanceof PersonSecurityRole)) { 14 | return false 15 | } 16 | 17 | other.person?.id == person?.id && 18 | other.securityRole?.id == securityRole?.id 19 | } 20 | 21 | int hashCode() { 22 | def builder = new HashCodeBuilder() 23 | if (person) builder.append(person.id) 24 | if (securityRole) builder.append(securityRole.id) 25 | builder.toHashCode() 26 | } 27 | 28 | static PersonSecurityRole get(long personId, long securityRoleId) { 29 | PersonSecurityRole.where { 30 | person == Person.load(personId) && 31 | securityRole == SecurityRole.load(securityRoleId) 32 | }.get() 33 | } 34 | 35 | static boolean exists(long personId, long securityRoleId) { 36 | PersonSecurityRole.where { 37 | person == Person.load(personId) && 38 | securityRole == SecurityRole.load(securityRoleId) 39 | }.count() > 0 40 | } 41 | 42 | static PersonSecurityRole create(Person person, SecurityRole securityRole, boolean flush = false) { 43 | def instance = new PersonSecurityRole(person: person, securityRole: securityRole) 44 | instance.save(flush: flush, insert: true) 45 | instance 46 | } 47 | 48 | static boolean remove(Person u, SecurityRole r, boolean flush = false) { 49 | if (u == null || r == null) return false 50 | 51 | int rowCount = PersonSecurityRole.where { 52 | person == Person.load(u.id) && 53 | securityRole == SecurityRole.load(r.id) 54 | }.deleteAll() 55 | 56 | if (flush) { PersonSecurityRole.withSession { it.flush() } } 57 | 58 | rowCount > 0 59 | } 60 | 61 | static void removeAll(Person u, boolean flush = false) { 62 | if (u == null) return 63 | 64 | PersonSecurityRole.where { 65 | person == Person.load(u.id) 66 | }.deleteAll() 67 | 68 | if (flush) { PersonSecurityRole.withSession { it.flush() } } 69 | } 70 | 71 | static void removeAll(SecurityRole r, boolean flush = false) { 72 | if (r == null) return 73 | 74 | PersonSecurityRole.where { 75 | securityRole == SecurityRole.load(r.id) 76 | }.deleteAll() 77 | 78 | if (flush) { PersonSecurityRole.withSession { it.flush() } } 79 | } 80 | 81 | static constraints = { 82 | securityRole validator: { SecurityRole r, PersonSecurityRole ur -> 83 | if (ur.person == null) return 84 | boolean existing = false 85 | PersonSecurityRole.withNewSession { 86 | existing = PersonSecurityRole.exists(ur.person.id, r.id) 87 | } 88 | if (existing) { 89 | return 'personSecurityRole.exists' 90 | } 91 | } 92 | } 93 | 94 | static mapping = { 95 | id composite: ['securityRole', 'person'] 96 | version false 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /grails-app/domain/demo/SecurityRole.groovy: -------------------------------------------------------------------------------- 1 | package demo 2 | 3 | class SecurityRole { 4 | 5 | String authority 6 | 7 | static mapping = { 8 | cache true 9 | } 10 | 11 | static constraints = { 12 | authority blank: false, unique: true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /grails-app/i18n/messages.properties: -------------------------------------------------------------------------------- 1 | default.doesnt.match.message=Property [{0}] of class [{1}] with value [{2}] does not match the required pattern [{3}] 2 | default.invalid.url.message=Property [{0}] of class [{1}] with value [{2}] is not a valid URL 3 | default.invalid.creditCard.message=Property [{0}] of class [{1}] with value [{2}] is not a valid credit card number 4 | default.invalid.email.message=Property [{0}] of class [{1}] with value [{2}] is not a valid e-mail address 5 | default.invalid.range.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid range from [{3}] to [{4}] 6 | default.invalid.size.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid size range from [{3}] to [{4}] 7 | default.invalid.max.message=Property [{0}] of class [{1}] with value [{2}] exceeds maximum value [{3}] 8 | default.invalid.min.message=Property [{0}] of class [{1}] with value [{2}] is less than minimum value [{3}] 9 | default.invalid.max.size.message=Property [{0}] of class [{1}] with value [{2}] exceeds the maximum size of [{3}] 10 | default.invalid.min.size.message=Property [{0}] of class [{1}] with value [{2}] is less than the minimum size of [{3}] 11 | default.invalid.validator.message=Property [{0}] of class [{1}] with value [{2}] does not pass custom validation 12 | default.not.inlist.message=Property [{0}] of class [{1}] with value [{2}] is not contained within the list [{3}] 13 | default.blank.message=Property [{0}] of class [{1}] cannot be blank 14 | default.not.equal.message=Property [{0}] of class [{1}] with value [{2}] cannot equal [{3}] 15 | default.null.message=Property [{0}] of class [{1}] cannot be null 16 | default.not.unique.message=Property [{0}] of class [{1}] with value [{2}] must be unique 17 | 18 | default.paginate.prev=Previous 19 | default.paginate.next=Next 20 | default.boolean.true=True 21 | default.boolean.false=False 22 | default.date.format=yyyy-MM-dd HH:mm:ss z 23 | default.number.format=0 24 | 25 | default.created.message={0} {1} created 26 | default.updated.message={0} {1} updated 27 | default.deleted.message={0} {1} deleted 28 | default.not.deleted.message={0} {1} could not be deleted 29 | default.not.found.message={0} not found with id {1} 30 | default.optimistic.locking.failure=Another user has updated this {0} while you were editing 31 | 32 | default.home.label=Home 33 | default.list.label={0} List 34 | default.add.label=Add {0} 35 | default.new.label=New {0} 36 | default.create.label=Create {0} 37 | default.show.label=Show {0} 38 | default.edit.label=Edit {0} 39 | 40 | default.button.create.label=Create 41 | default.button.edit.label=Edit 42 | default.button.update.label=Update 43 | default.button.delete.label=Delete 44 | default.button.delete.confirm.message=Are you sure? 45 | 46 | # Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) 47 | typeMismatch.java.net.URL=Property {0} must be a valid URL 48 | typeMismatch.java.net.URI=Property {0} must be a valid URI 49 | typeMismatch.java.util.Date=Property {0} must be a valid Date 50 | typeMismatch.java.lang.Double=Property {0} must be a valid number 51 | typeMismatch.java.lang.Integer=Property {0} must be a valid number 52 | typeMismatch.java.lang.Long=Property {0} must be a valid number 53 | typeMismatch.java.lang.Short=Property {0} must be a valid number 54 | typeMismatch.java.math.BigDecimal=Property {0} must be a valid number 55 | typeMismatch.java.math.BigInteger=Property {0} must be a valid number 56 | -------------------------------------------------------------------------------- /grails-app/init/BootStrap.groovy: -------------------------------------------------------------------------------- 1 | import grails.util.Environment 2 | import demo.* 3 | 4 | class BootStrap { 5 | 6 | def init = { servletContext -> 7 | if (Environment.TEST == Environment.current) { 8 | PersonSecurityRole.executeUpdate("delete PersonSecurityRole") 9 | SecurityRole.executeUpdate("delete SecurityRole") 10 | Person.executeUpdate("delete Person") 11 | } 12 | 13 | def adminRole = SecurityRole.findByAuthority('ROLE_ADMIN') ?: new SecurityRole(authority: 'ROLE_ADMIN').save(failOnError: true) 14 | def userRole = SecurityRole.findByAuthority('ROLE_CLIENT') ?: new SecurityRole(authority: 'ROLE_CLIENT').save(failOnError: true) 15 | 16 | def user1 = Person.findByUsername('bobbywarner') ?: new Person(username: 'bobbywarner', email: 'bobbywarner@gmail.com', password: 'xyz', fullName: 'Bobby Warner').save(failOnError: true) 17 | if (!user1.securityRoles.contains(userRole)) { 18 | PersonSecurityRole.create user1, userRole, true 19 | } 20 | if (!user1.securityRoles.contains(adminRole)) { 21 | PersonSecurityRole.create user1, adminRole, true 22 | } 23 | } 24 | 25 | def destroy = { 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /grails-app/init/demo/Application.groovy: -------------------------------------------------------------------------------- 1 | package demo 2 | 3 | import grails.boot.GrailsApp 4 | import grails.boot.config.GrailsAutoConfiguration 5 | import org.springframework.context.annotation.ComponentScan 6 | 7 | @ComponentScan 8 | class Application extends GrailsAutoConfiguration { 9 | static void main(String[] args) { 10 | GrailsApp.run(Application) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /grails-app/init/demo/OAuth2ServerConfiguration.groovy: -------------------------------------------------------------------------------- 1 | package demo 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Primary; 8 | import org.springframework.security.authentication.AuthenticationManager 9 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 11 | import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; 12 | import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; 13 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; 14 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 15 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; 16 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; 17 | import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; 18 | import org.springframework.security.oauth2.provider.token.DefaultTokenServices; 19 | import org.springframework.security.oauth2.provider.token.TokenStore; 20 | import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore 21 | import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore 22 | 23 | import javax.sql.DataSource; 24 | 25 | @Configuration 26 | class OAuth2ServerConfiguration { 27 | static final String RESOURCE_ID = 'demo' 28 | 29 | @Configuration 30 | @EnableResourceServer 31 | @EnableGlobalMethodSecurity(jsr250Enabled = true) 32 | protected static class ResourceServerConfiguration extends 33 | ResourceServerConfigurerAdapter { 34 | 35 | @Override 36 | public void configure(ResourceServerSecurityConfigurer resources) { 37 | resources 38 | .resourceId(RESOURCE_ID) 39 | } 40 | 41 | @Override 42 | public void configure(HttpSecurity http) throws Exception { 43 | http 44 | .authorizeRequests() 45 | .anyRequest().authenticated() 46 | } 47 | } 48 | 49 | @Configuration 50 | @EnableAuthorizationServer 51 | protected static class AuthorizationServerConfiguration extends 52 | AuthorizationServerConfigurerAdapter { 53 | 54 | @Autowired 55 | private DataSource dataSource 56 | 57 | @Autowired 58 | @Qualifier("authenticationManagerBean") 59 | private AuthenticationManager authenticationManager 60 | 61 | @Autowired 62 | private CustomUserDetailsService userDetailsService 63 | 64 | @Override 65 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { 66 | endpoints 67 | .tokenStore(tokenStore()) 68 | .authenticationManager(this.authenticationManager) 69 | .userDetailsService(userDetailsService) 70 | } 71 | 72 | @Override 73 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 74 | clients.jdbc(dataSource) 75 | } 76 | 77 | @Bean 78 | @Primary 79 | public DefaultTokenServices tokenServices() { 80 | DefaultTokenServices tokenServices = new DefaultTokenServices(); 81 | tokenServices.setSupportRefreshToken(true) 82 | tokenServices.setTokenStore(tokenStore()) 83 | return tokenServices 84 | } 85 | 86 | @Bean 87 | public TokenStore tokenStore() { 88 | return new JdbcTokenStore(dataSource); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /grails-app/init/demo/WebSecurityConfiguration.groovy: -------------------------------------------------------------------------------- 1 | package demo 2 | 3 | import org.springframework.beans.factory.annotation.Autowired 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import org.springframework.security.authentication.AuthenticationManager 7 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder 8 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity 9 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter 10 | import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity 11 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder 12 | 13 | @Configuration 14 | @EnableWebSecurity 15 | @EnableWebMvcSecurity 16 | public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { 17 | 18 | @Autowired 19 | CustomUserDetailsService userDetailsService 20 | 21 | @Override 22 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 23 | BCryptPasswordEncoder encoder = passwordEncoder(); 24 | auth 25 | .userDetailsService(userDetailsService) 26 | .passwordEncoder(encoder); 27 | } 28 | 29 | @Override 30 | @Bean 31 | public AuthenticationManager authenticationManagerBean() throws Exception { 32 | super.authenticationManagerBean() 33 | } 34 | 35 | @Bean 36 | public BCryptPasswordEncoder passwordEncoder() { 37 | return new BCryptPasswordEncoder(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /grails-app/migrations/changelog-security.groovy: -------------------------------------------------------------------------------- 1 | databaseChangeLog = { 2 | changeSet(author: "bobbywarner", id: "1430253322325-1") { 3 | createSequence(sequenceName: "hibernate_sequence") 4 | } 5 | 6 | changeSet(author: "bobbywarner", id: "1430253322325-2") { 7 | createTable(tableName: "person") { 8 | column(autoIncrement: "true", name: "id", type: "BIGINT") { 9 | constraints(primaryKey: "true", primaryKeyName: "personPK") 10 | } 11 | column(name: "version", type: "BIGINT") { 12 | constraints(nullable: "false") 13 | } 14 | column(name: "full_name", type: "VARCHAR(255)") { 15 | constraints(nullable: "false") 16 | } 17 | column(name: "email", type: "VARCHAR(255)") { 18 | constraints(nullable: "false") 19 | } 20 | column(name: "password", type: "VARCHAR(255)") { 21 | constraints(nullable: "false") 22 | } 23 | column(name: "username", type: "VARCHAR(255)") { 24 | constraints(nullable: "false") 25 | } 26 | } 27 | } 28 | 29 | changeSet(author: "bobbywarner", id: "1430253322325-3") { 30 | createTable(tableName: "person_security_role") { 31 | column(name: "security_role_id", type: "BIGINT") { 32 | constraints(nullable: "false") 33 | } 34 | column(name: "person_id", type: "BIGINT") { 35 | constraints(nullable: "false") 36 | } 37 | } 38 | } 39 | 40 | changeSet(author: "bobbywarner", id: "1430253322325-4") { 41 | createTable(tableName: "security_role") { 42 | column(autoIncrement: "true", name: "id", type: "BIGINT") { 43 | constraints(primaryKey: "true", primaryKeyName: "security_rolePK") 44 | } 45 | column(name: "version", type: "BIGINT") { 46 | constraints(nullable: "false") 47 | } 48 | column(name: "authority", type: "VARCHAR(255)") { 49 | constraints(nullable: "false") 50 | } 51 | } 52 | } 53 | 54 | changeSet(author: "bobbywarner", id: "1430253322325-5") { 55 | addPrimaryKey(columnNames: "security_role_id, person_id", constraintName: "person_security_rolePK", tableName: "person_security_role") 56 | } 57 | 58 | changeSet(author: "bobbywarner", id: "1430253322325-6") { 59 | addUniqueConstraint(columnNames: "username", constraintName: "UC_PERSONUSERNAME_COL", deferrable: "false", disabled: "false", initiallyDeferred: "false", tableName: "person") 60 | addUniqueConstraint(columnNames: "email", constraintName: "UC_PERSONEMAIL_COL", deferrable: "false", disabled: "false", initiallyDeferred: "false", tableName: "person") 61 | } 62 | 63 | changeSet(author: "bobbywarner", id: "1430253322325-7") { 64 | addUniqueConstraint(columnNames: "authority", constraintName: "UC_SECURITY_ROLEAUTHORITY_COL", deferrable: "false", disabled: "false", initiallyDeferred: "false", tableName: "security_role") 65 | } 66 | 67 | changeSet(author: "bobbywarner", id: "1430253322325-8") { 68 | addForeignKeyConstraint(baseColumnNames: "person_id", baseTableName: "person_security_role", constraintName: "FK_a4m7f8l95pxepnu3b5uv4hpkt", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "person") 69 | } 70 | 71 | changeSet(author: "bobbywarner", id: "1430253322325-9") { 72 | addForeignKeyConstraint(baseColumnNames: "security_role_id", baseTableName: "person_security_role", constraintName: "FK_k2fmduslrakqpgc7pcinl9xxx", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "security_role") 73 | } 74 | 75 | changeSet(author: "bobbywarner", id: "1430253322325-10") { 76 | createTable(tableName: "oauth_access_token") { 77 | column(name: "token_id", type: "VARCHAR(256)") 78 | column(name: "token", type: "BYTEA") 79 | column(name: "authentication_id", type: "VARCHAR(256)") 80 | column(name: "user_name", type: "VARCHAR(256)") 81 | column(name: "client_id", type: "VARCHAR(256)") 82 | column(name: "authentication", type: "BYTEA") 83 | column(name: "refresh_token", type: "VARCHAR(256)") 84 | } 85 | } 86 | 87 | changeSet(author: "bobbywarner", id: "1430253322325-11") { 88 | createTable(tableName: "oauth_approvals") { 89 | column(name: "userid", type: "VARCHAR(256)") 90 | column(name: "clientid", type: "VARCHAR(256)") 91 | column(name: "scope", type: "VARCHAR(256)") 92 | column(name: "status", type: "VARCHAR(10)") 93 | column(name: "expiresat", type: "TIMESTAMP WITHOUT TIME ZONE") 94 | column(name: "lastmodifiedat", type: "TIMESTAMP WITHOUT TIME ZONE") 95 | } 96 | } 97 | 98 | changeSet(author: "bobbywarner", id: "1430253322325-12") { 99 | createTable(tableName: "oauth_client_details") { 100 | column(name: "client_id", type: "VARCHAR(256)") { 101 | constraints(nullable: "false") 102 | } 103 | column(name: "resource_ids", type: "VARCHAR(256)") 104 | column(name: "client_secret", type: "VARCHAR(256)") 105 | column(name: "scope", type: "VARCHAR(256)") 106 | column(name: "authorized_grant_types", type: "VARCHAR(256)") 107 | column(name: "web_server_redirect_uri", type: "VARCHAR(256)") 108 | column(name: "authorities", type: "VARCHAR(256)") 109 | column(name: "access_token_validity", type: "INT4") 110 | column(name: "refresh_token_validity", type: "INT4") 111 | column(name: "additional_information", type: "VARCHAR(4096)") 112 | column(name: "autoapprove", type: "VARCHAR(256)") 113 | } 114 | } 115 | 116 | changeSet(author: "bobbywarner", id: "1430253322325-13") { 117 | createTable(tableName: "oauth_client_token") { 118 | column(name: "token_id", type: "VARCHAR(256)") 119 | column(name: "token", type: "BYTEA") 120 | column(name: "authentication_id", type: "VARCHAR(256)") 121 | column(name: "user_name", type: "VARCHAR(256)") 122 | column(name: "client_id", type: "VARCHAR(256)") 123 | } 124 | } 125 | 126 | changeSet(author: "bobbywarner", id: "1430253322325-14") { 127 | createTable(tableName: "oauth_code") { 128 | column(name: "code", type: "VARCHAR(256)") 129 | column(name: "authentication", type: "BYTEA") 130 | } 131 | } 132 | 133 | changeSet(author: "bobbywarner", id: "1430253322325-15") { 134 | createTable(tableName: "oauth_refresh_token") { 135 | column(name: "token_id", type: "VARCHAR(256)") 136 | column(name: "token", type: "BYTEA") 137 | column(name: "authentication", type: "BYTEA") 138 | } 139 | } 140 | 141 | changeSet(author: "bobbywarner", id: "1430253322325-16") { 142 | addPrimaryKey(columnNames: "client_id", constraintName: "oauth_client_details_pkey", tableName: "oauth_client_details") 143 | } 144 | 145 | changeSet(author: "bobbywarner", id: "1430253322325-17") { 146 | insert(tableName: 'oauth_client_details') { 147 | column(name: "client_id", value: "demo-client") 148 | column(name: "resource_ids", value: "demo") 149 | column(name: "client_secret", value: "123456") 150 | column(name: "scope", value: "read,write") 151 | column(name: "authorized_grant_types", value: "password,refresh_token,client_credentials") 152 | column(name: "web_server_redirect_uri", value: "") 153 | column(name: "authorities", value: "ROLE_CLIENT,ROLE_GUEST") 154 | column(name: "access_token_validity", value: 43200) 155 | column(name: "refresh_token_validity", value: 2592000) 156 | column(name: "additional_information", value: "") 157 | column(name: "autoapprove", value: false) 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /grails-app/migrations/changelog.groovy: -------------------------------------------------------------------------------- 1 | databaseChangeLog = { 2 | include file: 'changelog-security.groovy' 3 | } 4 | -------------------------------------------------------------------------------- /grails-app/services/demo/CustomUserDetailsService.groovy: -------------------------------------------------------------------------------- 1 | package demo 2 | 3 | import org.springframework.security.core.GrantedAuthority 4 | import org.springframework.security.core.authority.SimpleGrantedAuthority 5 | import org.springframework.security.core.userdetails.User 6 | import org.springframework.security.core.userdetails.UserDetails 7 | import org.springframework.security.core.userdetails.UserDetailsService 8 | import org.springframework.security.core.userdetails.UsernameNotFoundException 9 | 10 | class CustomUserDetailsService implements UserDetailsService { 11 | 12 | @Override 13 | UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 14 | Person person = Person.findByUsername(username) 15 | if (person == null) { 16 | throw new UsernameNotFoundException(String.format("User %s does not exist!", username)) 17 | } 18 | new User(person.username, person.password, loadAuthorities(person)) 19 | } 20 | 21 | private Collection loadAuthorities(Person person) { 22 | Collection userAuthorities = person.securityRoles 23 | userAuthorities.collect { new SimpleGrantedAuthority(it.authority) } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /grails-app/views/error.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <g:if env="development">Grails Runtime Exception</g:if><g:else>Error</g:else> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
    17 |
  • An error has occurred
  • 18 |
  • Exception: ${exception}
  • 19 |
  • Message: ${message}
  • 20 |
  • Path: ${path}
  • 21 |
22 |
23 |
24 | 25 |
    26 |
  • An error has occurred
  • 27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /grails-app/views/index.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo 6 | 17 | 18 | 19 |

Demo API

20 |

This is a demo project using Grails 3 and Spring Security OAuth.

21 | 22 | 23 | -------------------------------------------------------------------------------- /grails-app/views/notFound.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page Not Found 5 | 6 | 7 | 8 | 9 |
    10 |
  • Error: Page Not Found (404)
  • 11 |
  • Path: ${request.forwardURI}
  • 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /sql/setup.sql: -------------------------------------------------------------------------------- 1 | drop database if exists demo_test; 2 | drop user if exists demo_test; 3 | drop database if exists demo_dev; 4 | drop user if exists demo_dev; 5 | create database demo_test; 6 | create user demo_test; 7 | \c demo_test 8 | GRANT ALL PRIVILEGES ON schema public to demo_test; 9 | create database demo_dev; 10 | create user demo_dev; 11 | \c demo_dev 12 | GRANT ALL PRIVILEGES ON schema public to demo_dev; 13 | -------------------------------------------------------------------------------- /src/integration-test/groovy/demo/AuthenticatedSpec.groovy: -------------------------------------------------------------------------------- 1 | package demo 2 | 3 | import grails.util.Holders 4 | import org.springframework.util.LinkedMultiValueMap 5 | import org.springframework.util.MultiValueMap 6 | import spock.lang.* 7 | import grails.plugins.rest.client.RestBuilder 8 | 9 | abstract class AuthenticatedSpec extends Specification { 10 | String baseUrl 11 | RestBuilder rest 12 | String accessToken 13 | String refreshToken 14 | 15 | def setup() { 16 | baseUrl = Holders.grailsApplication.config.grails.serverURL 17 | rest = new RestBuilder() 18 | MultiValueMap params = new LinkedMultiValueMap() 19 | params.add('username', 'bobbywarner') 20 | params.add('password', 'xyz') 21 | params.add('grant_type', 'password') 22 | params.add('scope', 'read write') 23 | params.add('client_secret', '123456') 24 | params.add('client_id', 'demo-client') 25 | 26 | def response = rest.post("${baseUrl}/oauth/token") { 27 | auth(params.getFirst('client_id'), params.getFirst('client_secret')) 28 | accept("application/json") 29 | contentType("application/x-www-form-urlencoded") 30 | body(params) 31 | } 32 | 33 | response.status == 200 34 | assert 'read write' == response.json.scope 35 | assert 'bearer' == response.json.token_type 36 | accessToken = "Bearer $response.json.access_token" 37 | refreshToken = response.json.refresh_token 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/integration-test/groovy/demo/ProfileSpec.groovy: -------------------------------------------------------------------------------- 1 | package demo 2 | 3 | import grails.test.mixin.integration.Integration 4 | import grails.transaction.Rollback 5 | import grails.util.Holders 6 | 7 | @Integration 8 | @Rollback 9 | class ProfileSpec extends AuthenticatedSpec { 10 | 11 | void "get profile"() { 12 | when: 13 | def response = rest.get("${baseUrl}/profile") { 14 | header("Authorization", accessToken) 15 | } 16 | 17 | then: 18 | response.status == 200 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/integration-test/groovy/demo/RegisterSpec.groovy: -------------------------------------------------------------------------------- 1 | package demo 2 | 3 | import grails.test.mixin.integration.Integration 4 | import grails.transaction.Rollback 5 | import grails.util.Holders 6 | import spock.lang.Specification 7 | import grails.plugins.rest.client.RestBuilder 8 | 9 | @Integration 10 | @Rollback 11 | class RegisterSpec extends Specification { 12 | String baseUrl 13 | RestBuilder rest 14 | String accessToken 15 | 16 | def setup() { 17 | baseUrl = Holders.grailsApplication.config.grails.serverURL 18 | rest = new RestBuilder() 19 | def response = rest.post("${baseUrl}/oauth/token?grant_type=client_credentials") { 20 | auth('demo-client', '123456') 21 | accept("application/json") 22 | } 23 | 24 | response.status == 200 25 | assert 'read write' == response.json.scope 26 | assert 'bearer' == response.json.token_type 27 | accessToken = "Bearer $response.json.access_token" 28 | } 29 | 30 | void "successful signup"() { 31 | when: 32 | def response = rest.post("${baseUrl}/register") { 33 | header("Authorization", accessToken) 34 | contentType('application/json') 35 | json { 36 | fullName = 'John Doe' 37 | username = 'johndoe' 38 | email = 'johndoe@gmail.com' 39 | password = 'password' 40 | } 41 | } 42 | 43 | then: 44 | response.status == 201 45 | response.json.fullName == 'John Doe' 46 | response.json.username == 'johndoe' 47 | response.json.email == 'johndoe@gmail.com' 48 | } 49 | 50 | void "email address already taken"() { 51 | when: 52 | def response = rest.post("${baseUrl}/register") { 53 | header("Authorization", accessToken) 54 | contentType('application/json') 55 | json { 56 | fullName = 'Bobby Warner' 57 | username = 'bobby' 58 | email = 'bobbywarner@gmail.com' 59 | password = 'password' 60 | } 61 | } 62 | 63 | then: 64 | response.status == 400 65 | response.json.errors.first().message == "A user already exists with this email address" 66 | } 67 | 68 | void "username already taken"() { 69 | when: 70 | def response = rest.post("${baseUrl}/register") { 71 | header("Authorization", accessToken) 72 | contentType('application/json') 73 | json { 74 | fullName = 'Bobby Warner' 75 | username = 'bobbywarner' 76 | email = 'bobby@gmail.com' 77 | password = 'password' 78 | } 79 | } 80 | 81 | then: 82 | response.status == 400 83 | response.json.errors.first().message == "A user already exists with this username" 84 | } 85 | 86 | void "invalid email address"() { 87 | when: 88 | def response = rest.post("${baseUrl}/register") { 89 | header("Authorization", accessToken) 90 | contentType('application/json') 91 | json { 92 | fullName = 'John Doe' 93 | username = 'johndoe' 94 | email = 'not valid email' 95 | password = 'password' 96 | } 97 | } 98 | 99 | then: 100 | response.status == 400 101 | response.json.errors != null 102 | } 103 | 104 | void "unsuccessful signup"() { 105 | when: 106 | def response = rest.post("${baseUrl}/register") { 107 | header("Authorization", accessToken) 108 | } 109 | 110 | then: 111 | response.status == 400 112 | response.json.errors != null 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/integration-test/groovy/demo/UnauthenticatedSpec.groovy: -------------------------------------------------------------------------------- 1 | package demo 2 | 3 | import grails.test.mixin.integration.Integration 4 | import grails.transaction.* 5 | import grails.util.Holders 6 | import spock.lang.* 7 | import grails.plugins.rest.client.RestBuilder 8 | import grails.plugins.rest.client.RestResponse 9 | 10 | @Integration 11 | @Rollback 12 | class UnauthenticatedSpec extends Specification { 13 | String baseUrl 14 | RestBuilder rest 15 | 16 | def setup() { 17 | baseUrl = Holders.grailsApplication.config.grails.serverURL 18 | rest = new RestBuilder() 19 | } 20 | 21 | void "api is secure and returns 401"() { 22 | when: 23 | RestResponse response = rest.get(baseUrl) 24 | 25 | then: 26 | response.status == 401 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/groovy/demo/marshallers/PersonMarshallerJson.groovy: -------------------------------------------------------------------------------- 1 | package demo.marshallers 2 | 3 | import org.grails.web.converters.marshaller.ClosureObjectMarshaller 4 | import demo.Person 5 | 6 | class PersonMarshallerJson extends ClosureObjectMarshaller { 7 | 8 | public static marshal = { Person person -> 9 | def json = [:] 10 | json.fullName = person.fullName 11 | json.email = person.email 12 | json.username = person.username 13 | json 14 | } 15 | 16 | public PersonMarshallerJson() { 17 | super(Person, marshal) 18 | } 19 | } 20 | --------------------------------------------------------------------------------