Skip to content

Commit c30a0d4

Browse files
committed
OIDC Provider for the grails-spring-security-oauth2 plugin
1 parent 0ac6815 commit c30a0d4

File tree

16 files changed

+557
-114
lines changed

16 files changed

+557
-114
lines changed

.sdkmanrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Enable auto-env through the sdkman_auto_env config
2+
# Add key=value pairs of SDKs to use below
3+
java=17.0.15-librca

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
Spring Security OAuth2 OIDC Plugin
2+
====================================
3+
[ ![Download](https://api.bintray.com/packages/grails/plugins/spring-security-oauth2-google/images/download.svg) ](https://bintray.com/grails/plugins/spring-security-oauth2-google/_latestVersion)
4+
5+
Add aa OIDC OAuth2 provider to the [Spring Security OAuth2 Plugin](https://github.com/apache/grails-/grails-spring-security-oauth2).
6+
7+
Installation
8+
------------
9+
Add the following dependencies in `build.gradle`
10+
```
11+
dependencies {
12+
...
13+
compile 'org.apache.grails:grails-spring-security-oauth2:7.0.0-M5'
14+
compile 'org.grails.plugins:spring-security-oauth2-oidc:1.0.+'
15+
...
16+
}
17+
```
18+
19+
Usage
20+
-----
21+
Add this to your application.yml
22+
```
23+
grails:
24+
plugin:
25+
springsecurity:
26+
oauth2:
27+
providers:
28+
oidc:
29+
api_key: 'oidc-api-key' #needed
30+
api_secret: 'oidc-api-secret' #needed
31+
successUri: "/oauth2/google/success" #optional
32+
failureUri: "/oauth2/google/failure" #optional
33+
callback: "/oauth2/google/callback" #optional
34+
scopes: "some_scope" #optional, see https://developers.google.com/identity/protocols/googlescopes#monitoringv3
35+
```
36+
You can replace the URIs with your own controller implementation.
37+
38+
In your view you can use the taglib exposed from this plugin and from OAuth plugin to create links and to know if the user is authenticated with a given provider:
39+
```xml
40+
<oauth2:connect provider="google" id="google-connect-link">Google</oauth2:connect>
41+
42+
Logged with google?
43+
<oauth2:ifLoggedInWith provider="google">yes</oauth2:ifLoggedInWith>
44+
<oauth2:ifNotLoggedInWith provider="google">no</oauth2:ifNotLoggedInWith>
45+
```
46+
License
47+
-------
48+
Apache 2

build.gradle

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import java.time.Instant
2+
import java.time.ZoneOffset
3+
import java.time.format.DateTimeFormatter
4+
15
buildscript {
26
repositories {
37
mavenCentral()
@@ -14,13 +18,31 @@ buildscript {
1418
classpath "org.apache.grails:grails-gradle-plugins"
1519
}
1620
}
21+
apply plugin: 'groovy'
22+
apply plugin: 'java-library'
23+
apply plugin: 'org.apache.grails.gradle.grails-plugin'
24+
apply plugin: 'org.apache.grails.gradle.grails-publish'
25+
26+
group "org.grails.plugins"
1727

18-
version "0.1"
19-
group "grails.spring.security.oauth.oidc"
28+
ext {
29+
buildInstant = java.util.Optional.ofNullable(System.getenv("SOURCE_DATE_EPOCH"))
30+
.filter(s -> !s.isEmpty())
31+
.map(Long::parseLong)
32+
.map(Instant::ofEpochSecond)
33+
.orElseGet(Instant::now)
34+
formattedBuildDate = DateTimeFormatter.ISO_INSTANT.format(buildInstant)
35+
buildDate = (buildInstant as Instant).atZone(ZoneOffset.UTC) // for reproducible builds
36+
37+
publishArtifactId = 'grails-spring-security-oauth2-oidc'
38+
pomTitle = 'Grails Spring Security OAuth2 OIDC Provider Plugin'
39+
pomDescription = 'This plugin provides the oauth2 OIDC provider for grails-spring-security-oauth2 plugin.'
40+
pomDevelopers = [
41+
sbglasius: 'Søren Berg Glasius',
42+
]
43+
44+
}
2045

21-
apply plugin:"eclipse"
22-
apply plugin:"idea"
23-
apply plugin:"org.apache.grails.gradle.grails-plugin"
2446

2547
repositories {
2648
mavenCentral()
@@ -35,23 +57,34 @@ repositories {
3557

3658
dependencies {
3759
implementation platform("org.apache.grails:grails-bom:$grailsVersion")
38-
developmentOnly "org.springframework.boot:spring-boot-devtools" // Spring Boot DevTools may cause performance slowdowns or compatibility issues on larger applications
39-
integrationTestImplementation testFixtures("org.apache.grails:grails-geb")
40-
console "org.apache.grails:grails-console"
41-
implementation "org.springframework.boot:spring-boot-starter-logging"
42-
implementation "org.springframework.boot:spring-boot-starter-validation"
43-
implementation "org.springframework.boot:spring-boot-autoconfigure"
44-
implementation "org.springframework.boot:spring-boot-starter"
45-
implementation "org.apache.grails:grails-core"
46-
implementation "org.apache.grails:grails-i18n"
60+
compileOnly "org.springframework.boot:spring-boot-starter-logging"
61+
compileOnly "org.springframework.boot:spring-boot-starter-validation"
62+
compileOnly "org.springframework.boot:spring-boot-autoconfigure"
63+
compileOnly "org.springframework.boot:spring-boot-starter"
64+
compileOnly "org.apache.grails:grails-core"
4765
profile "org.apache.grails.profiles:plugin"
4866
runtimeOnly "org.fusesource.jansi:jansi"
49-
testImplementation "org.apache.grails:grails-testing-support-datamapping"
50-
testImplementation "org.spockframework:spock-core"
67+
68+
implementation 'org.apache.grails:grails-spring-security:7.0.0-M5'
69+
implementation 'org.apache.grails:grails-spring-security-oauth2:7.0.0-M5'
70+
implementation 'com.auth0:java-jwt:4.5.0'
71+
implementation 'com.auth0:jwks-rsa:0.23.0'
72+
}
73+
74+
grailsPublish {
75+
githubSlug = 'grails-plugins/grails-spring-security-oauth2-oidc'
76+
license {
77+
name = 'Apache-2.0'
78+
}
79+
title = pomTitle
80+
desc = pomDescription
81+
developers = pomDevelopers
5182
}
5283

53-
compileJava.options.release = 17
84+
85+
compileJava.options.release = javaVersion.toInteger()
5486

5587
tasks.withType(Test) {
5688
useJUnitPlatform()
5789
}
90+

buildSrc/build.gradle

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
plugins {
2+
id 'groovy-gradle-plugin'
3+
}
4+
5+
file('../gradle.properties').withInputStream {
6+
def gradleProperties = new Properties()
7+
gradleProperties.load(it)
8+
gradleProperties.each { k, v -> ext.set(k, v) }
9+
10+
}
11+
12+
repositories {
13+
maven { url = 'https://repo.grails.org/grails/restricted' }
14+
maven {
15+
url = 'https://repository.apache.org/content/groups/snapshots'
16+
content {
17+
includeVersionByRegex('org[.]apache[.](grails|groovy).*', '.*', '.*-SNAPSHOT')
18+
}
19+
}
20+
}
21+
22+
dependencies {
23+
implementation platform("org.apache.grails:grails-bom:$grailsVersion")
24+
implementation "org.asciidoctor.jvm.convert:org.asciidoctor.jvm.convert.gradle.plugin:$asciidoctorGradlePluginVersion"
25+
implementation 'org.apache.grails:grails-gradle-plugins'
26+
}

gradle.properties

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
grailsVersion=7.0.0-RC1
1+
version=1.0.0-SNAPSHOT
2+
grailsVersion=7.0.0-SNAPSHOT
3+
javaVersion=17
4+
5+
asciidoctorGradlePluginVersion=4.0.4
6+
gradleCryptoChecksumVersion=1.4.0
7+
ratVersion=0.8.1
8+
29
org.gradle.daemon=true
310
org.gradle.parallel=true
411
org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1024M
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/* Copyright 2025-2025 the original author or authors.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
security {
17+
oauth2 {
18+
providers {
19+
oidc {
20+
domain = 'https://some.oidc.provider.domain' // Mandatory
21+
api_key = "changeme_apikey" // Mandatory
22+
api_secret = "changeme_apisecret" // Mandatory
23+
successUri = "/oauth2/oidc/success" // Optional
24+
failureUri = "/oauth2/oidc/failure" // Optional
25+
callback = "/oauth2/oidc/callback" // Optional
26+
}
27+
}
28+
}
29+
}

grails-app/conf/application.yml

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
grails:
22
profile: plugin
33
codegen:
4-
defaultPackage: grails.spring.security.oauth.oidc
4+
defaultPackage: grails.springsecurity.oauth2.oidc
55
gorm:
66
reactor:
77
# Whether to translate GORM events into Reactor events
@@ -20,15 +20,6 @@ spring:
2020
groovy:
2121
template:
2222
check-template-location: false
23-
devtools:
24-
restart:
25-
additional-exclude:
26-
- '*.gsp'
27-
- '**/*.gsp'
28-
- '*.gson'
29-
- '**/*.gson'
30-
- 'logback-spring.xml'
31-
- '*.properties'
3223
environments:
3324
development:
3425
management:

grails-app/init/grails/spring/security/oauth/oidc/BootStrap.groovy

Lines changed: 0 additions & 15 deletions
This file was deleted.

grails-app/init/grails/spring/security/oauth/oidc/Application.groovy renamed to grails-app/init/grails/springsecurity/oauth2/oidc/Application.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package grails.spring.security.oauth.oidc
1+
package grails.springsecurity.oauth2.oidc
22

33
import grails.boot.*
44
import grails.boot.config.GrailsAutoConfiguration
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/* Copyright 2025-2025 the original author or authors.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
package grails.plugin.springsecurity.oauth2.oidc
17+
18+
import com.auth0.jwk.JwkProviderBuilder
19+
import com.auth0.jwt.JWT
20+
import com.auth0.jwt.JWTVerifier
21+
import com.auth0.jwt.algorithms.Algorithm
22+
import com.auth0.jwt.exceptions.JWTVerificationException
23+
import com.auth0.jwt.interfaces.DecodedJWT
24+
import com.auth0.jwt.interfaces.RSAKeyProvider
25+
import com.github.scribejava.core.builder.ServiceBuilder
26+
import com.github.scribejava.core.builder.api.DefaultApi20
27+
import com.github.scribejava.core.model.OAuth2AccessToken
28+
import com.github.scribejava.core.oauth.OAuth20Service
29+
import grails.plugin.springsecurity.oauth2.SpringSecurityOauth2BaseService
30+
import grails.plugin.springsecurity.oauth2.exception.OAuth2Exception
31+
import grails.plugin.springsecurity.oauth2.service.OAuth2AbstractProviderService
32+
import grails.plugin.springsecurity.oauth2.token.OAuth2SpringToken
33+
import grails.plugin.springsecurity.oauth2.util.OAuth2ProviderConfiguration
34+
import groovy.json.JsonSlurper
35+
import groovy.transform.CompileStatic
36+
import org.springframework.beans.factory.InitializingBean
37+
38+
import java.security.interfaces.RSAPrivateKey
39+
import java.security.interfaces.RSAPublicKey
40+
import java.util.concurrent.TimeUnit
41+
42+
@CompileStatic
43+
class OidcAuth2Service extends OAuth2AbstractProviderService implements InitializingBean{
44+
45+
SpringSecurityOauth2BaseService springSecurityOauth2BaseService
46+
47+
OidcOauth2Config OidcOauth2Config
48+
49+
final String providerID = OidcOauth2Api.PROVIDER_ID
50+
final Class apiClass = OidcOauth2Api
51+
final String scopeSeparator = ' '
52+
private JWTVerifier jwtVerifier
53+
54+
String getProfileScope() {
55+
"${OidcOauth2Config.domain}/userinfo"
56+
}
57+
String getScopes() {
58+
OidcOauth2Config.scope
59+
}
60+
61+
62+
@Override
63+
OAuth2SpringToken createSpringAuthToken(OAuth2AccessToken accessToken) {
64+
DecodedJWT decoded = decodeJWT(accessToken)
65+
verifyJWT(decoded)
66+
return new OidcOauth2SpringToken(accessToken, decoded.claims)
67+
}
68+
69+
DecodedJWT decodeJWT(OAuth2AccessToken accessToken) {
70+
def raw = new JsonSlurper().parseText(accessToken.rawResponse as String) as Map
71+
String idToken = raw?.id_token as String
72+
if (!idToken) throw new IllegalStateException("Missing id_token from OIDC response")
73+
74+
DecodedJWT decoded = new JWT().decodeJwt(idToken)
75+
return decoded
76+
}
77+
78+
@Override
79+
OAuth20Service buildScribeService(OAuth2ProviderConfiguration providerConfiguration) {
80+
DefaultApi20 api = new OidcOauth2Api(OidcOauth2Config.domain)
81+
82+
return new ServiceBuilder(OidcOauth2Config.apiKey)
83+
.apiSecret(OidcOauth2Config.apiSecret)
84+
.defaultScope(OidcOauth2Config.scope)
85+
.callback(providerConfiguration.callbackUrl)
86+
.build(api)
87+
}
88+
89+
90+
@Override
91+
void afterPropertiesSet() throws Exception {
92+
def jwkProvider = new JwkProviderBuilder(OidcOauth2Config.domain)
93+
.cached(10, 24, TimeUnit.HOURS)
94+
.rateLimited(10, 1, TimeUnit.MINUTES)
95+
.build()
96+
97+
RSAKeyProvider keyProvider = new RSAKeyProvider() {
98+
@Override
99+
RSAPublicKey getPublicKeyById(String kid) {
100+
return (RSAPublicKey) jwkProvider.get(kid).getPublicKey()
101+
}
102+
103+
@Override
104+
RSAPrivateKey getPrivateKey() {
105+
// return the private key used
106+
return null
107+
}
108+
109+
@Override
110+
String getPrivateKeyId() {
111+
return null
112+
}
113+
}
114+
115+
def algorithm = Algorithm.RSA256(keyProvider)
116+
String issuerDomain = OidcOauth2Config.domain.endsWith('/') ? OidcOauth2Config.domain : OidcOauth2Config.domain + '/'
117+
jwtVerifier = JWT.require(algorithm)
118+
.withIssuer(issuerDomain)
119+
.withAnyOfAudience(OidcOauth2Config.apiKey)
120+
.build()
121+
}
122+
123+
124+
private void verifyJWT(DecodedJWT decoded) {
125+
try {
126+
jwtVerifier.verify(decoded)
127+
} catch (JWTVerificationException e) {
128+
throw new OAuth2Exception("OIDC token is invalid", e)
129+
}
130+
}
131+
132+
}

0 commit comments

Comments
 (0)