Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Okta Spring Boot starter doesn't work with GraalVM #192

Closed
mraible opened this issue Sep 21, 2020 · 19 comments
Closed

Okta Spring Boot starter doesn't work with GraalVM #192

mraible opened this issue Sep 21, 2020 · 19 comments

Comments

@mraible
Copy link
Contributor

mraible commented Sep 21, 2020

Steps to reproduce:

Create a new Spring Boot app using HTTPie:

http https://start.spring.io/starter.zip \
     dependencies==web,okta \
     packageName==com.okta.rest \
     name=spring-boot \
     type=maven-project \
     -o spring-boot.zip

Then, add a HelloController:

package com.okta.rest.controller;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(@AuthenticationPrincipal Principal principal) {
        return "Hello, " + principal.getName() + "!";
    }
}

Configure it to be an OAuth 2.0 resource server by adding an issuer to application.properties:

okta.oauth2.issuer=https://dev-133337.okta.com/oauth2/default

All the following should work at this point (I got an access token from oidcdebugger.com):

$ mvn spring-boot:run
$ http :8080/hello
$ TOKEN=eyJraWQiOiJxOE1QMjFNNHZCVmxOSkxGbFFWNlN...
$ http :8080/hello Authorization:"Bearer $TOKEN"

To add "build native image support", I followed these docs.

I upgraded my app to use Spring Boot v2.4.0-M2.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.0-M2</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

I updated my configuration to avoid proxies:

@SpringBootApplication(proxyBeanMethods = false)
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

I added the milestone repos to my pom.xml:

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
    </repository>
</repositories>
<pluginRepositories>
    <pluginRepository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
    </pluginRepository>
</pluginRepositories>

I configured the Spring Boot Maven plugin:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <image>
            <builder>paketobuildpacks/builder:tiny</builder>
            <env>
                <BP_BOOT_NATIVE_IMAGE>1</BP_BOOT_NATIVE_IMAGE>
                <BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS>
                    -Dspring.native.remove-yaml-support=true
                    -Dspring.spel.ignore=true
                </BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS>
            </env>
        </image>
    </configuration>
</plugin>

I added the Spring GraalVM dependency:

<dependency>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-graalvm-native</artifactId>
    <version>0.8.0</version>
</dependency>

And built my application.

./mvnw spring-boot:build-image

Then I tried to run it.

docker run -p 8080:8080 docker.io/library/demo:0.0.1-SNAPSHOT

It fails with the following error:

2020-09-21 18:40:59.056  INFO 1 --- [           main] com.okta.rest.Application                : Starting Application using Java 11.0.8 on 089430f03b4c with PID 1 (/workspace/com.okta.rest.Application started by cnb in /workspace)
2020-09-21 18:40:59.056  INFO 1 --- [           main] com.okta.rest.Application                : No active profile set, falling back to default profiles: default
2020-09-21 18:40:59.098  WARN 1 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanDefinitionStoreException: @Configuration classes need to be marked as proxyBeanMethods=false. Found: [com.okta.spring.boot.oauth.OktaOAuth2ResourceServerAutoConfig]
2020-09-21 18:40:59.099  INFO 1 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2020-09-21 18:40:59.099 ERROR 1 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.BeanDefinitionStoreException: @Configuration classes need to be marked as proxyBeanMethods=false. Found: [com.okta.spring.boot.oauth.OktaOAuth2ResourceServerAutoConfig]
        at org.springframework.context.annotation.ConfigurationClassPostProcessor.enhanceConfigurationClasses(ConfigurationClassPostProcessor.java:436) ~[com.okta.rest.Application:na]
        at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanFactory(ConfigurationClassPostProcessor.java:273) ~[com.okta.rest.Application:na]
        at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:299) ~[na:na]
        at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:134) ~[na:na]
        at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:751) ~[na:na]
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:569) ~[na:na]
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:144) ~[na:na]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:759) ~[com.okta.rest.Application:na]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:751) ~[com.okta.rest.Application:na]
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:410) ~[com.okta.rest.Application:na]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:326) ~[com.okta.rest.Application:na]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1280) ~[com.okta.rest.Application:na]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1269) ~[com.okta.rest.Application:na]
        at com.okta.rest.Application.main(Application.java:10) ~[com.okta.rest.Application:na]                                                                    
@mraible
Copy link
Contributor Author

mraible commented Sep 21, 2020

I tried to switch to Spring Security's dependencies and configuration as a workaround. I replaced the Okta Spring Boot starter with:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

And I changed application.properties to use Spring Security's key:

spring.security.oauth2.resourceserver.jwt.issuer-uri=https://dev-133320.okta.com/oauth2/default

I can run everything just fine with mvn spring-boot:run. It builds with GraalVM too. However, when I try to run it with:

docker run -p 8080:8080 docker.io/library/demo:0.0.1-SNAPSHOT

I get an error on startup:

2020-09-21 23:43:05.538  INFO 1 --- [           main] com.okta.rest.Application                : Starting Application using Java 11.0.8 on 94f7aaa2abf1 with PID 1 (/workspace/com.okta.rest.Application started by cnb in /workspace)
2020-09-21 23:43:05.539  INFO 1 --- [           main] com.okta.rest.Application                : No active profile set, falling back to default profiles: default
2020-09-21 23:43:05.566 ERROR 1 --- [           main] o.s.boot.SpringApplication               : Application run failed

java.lang.IllegalStateException: java.security.NoSuchAlgorithmException: RSA KeyFactory not available
        at org.springframework.security.converter.RsaKeyConverters.rsaFactory(RsaKeyConverters.java:129) ~[na:na]
        at org.springframework.security.converter.RsaKeyConverters.pkcs8(RsaKeyConverters.java:63) ~[na:na]
        at org.springframework.security.config.crypto.RsaKeyConversionServicePostProcessor.pkcs8(RsaKeyConversionServicePostProcessor.java:89) ~[na:na]
        at org.springframework.security.config.crypto.RsaKeyConversionServicePostProcessor.postProcessBeanFactory(RsaKeyConversionServicePostProcessor.java:66) ~[na:na]
        at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:299) ~[na:na]
        at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:185) ~[na:na]
        at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:751) ~[na:na]
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:569) ~[na:na]
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:144) ~[na:na]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:759) ~[com.okta.rest.Application:na]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:751) ~[com.okta.rest.Application:na]
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:410) ~[com.okta.rest.Application:na]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:326) ~[com.okta.rest.Application:na]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1280) ~[com.okta.rest.Application:na]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1269) ~[com.okta.rest.Application:na]
        at com.okta.rest.Application.main(Application.java:10) ~[com.okta.rest.Application:na]
Caused by: java.security.NoSuchAlgorithmException: RSA KeyFactory not available
        at java.security.KeyFactory.<init>(KeyFactory.java:138) ~[na:na]
        at java.security.KeyFactory.getInstance(KeyFactory.java:183) ~[na:na]
        at org.springframework.security.converter.RsaKeyConverters.rsaFactory(RsaKeyConverters.java:127) ~[na:na]
        ... 15 common frames omitted

Any ideas @jgrandja?

@mraible
Copy link
Contributor Author

mraible commented Sep 22, 2020

I tried using the full builder as documented here.

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <image>
            <builder>paketobuildpacks/builder:full</builder>
            <env>
                <BP_BOOT_NATIVE_IMAGE>1</BP_BOOT_NATIVE_IMAGE>
                <BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS>
                    -Dspring.native.remove-yaml-support=true
                    -Dspring.spel.ignore=true
                </BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS>
            </env>
        </image>
    </configuration>
</plugin>

Unfortunately, the same error still happens.

@mraible
Copy link
Contributor Author

mraible commented Sep 22, 2020

Hmmm, the paketobuildpacks/builder repo says it's full-cf instead of full. Trying...

... that didn't work. Same error. I'll try upgrading to Spring Boot 2.4.0-M3 and a snapshot of spring-graalvm-native.

Nope, can't get a snapshot of spring-graalvm-native, even after adding snapshot repos.

<repository>
    <id>spring-snapshots</id>
    <name>Spring Snapshot Repository</name>
    <url>http://repo.spring.io/snapshot</url>
</repository>

Error is:

[ERROR] Failed to execute goal on project demo: Could not resolve dependencies for project com.example:demo:jar:0.0.1-SNAPSHOT: 
Failed to collect dependencies at org.springframework.experimental:spring-graalvm-native:jar:0.8.1-SNAPSHOT: 
Failed to read artifact descriptor for org.springframework.experimental:spring-graalvm-native:jar:0.8.1-SNAPSHOT: 
Could not transfer artifact org.springframework.experimental:spring-graalvm-native:pom:0.8.1-SNAPSHOT from/to spring-snapshots (http://repo.spring.io/snapshot): 

Authorization failed for http://repo.spring.io/snapshot/org/springframework/experimental/spring-graalvm-native/0.8.1-SNAPSHOT/spring-graalvm-native-0.8.1-SNAPSHOT.pom 
403 Forbidden -> [Help 1]

I tried to clone and build spring-graalvm-native, but that fails too:

➜  spring-graalvm-native git:(master) mvn install -DskipTests
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] Spring GraalVM Native build                                        [pom]
[INFO] Spring GraalVM Native feature                                      [jar]
[INFO] Spring GraalVM Native configuration                                [jar]
[INFO] Spring GraalVM Native substitutions                                [jar]
[INFO] Spring GraalVM Native tools                                        [jar]
[INFO] Spring GraalVM Native docs                                         [pom]
[INFO] Spring GraalVM Native                                              [jar]
[INFO]
[INFO] ----< org.springframework.experimental:spring-graalvm-native-build >----
[INFO] Building Spring GraalVM Native build 0.8.1-SNAPSHOT                [1/7]
[INFO] --------------------------------[ pom ]---------------------------------
[INFO]
[INFO] --- maven-install-plugin:2.4:install (default-install) @ spring-graalvm-native-build ---
[INFO] Installing /Users/mraible/dev/spring-graalvm-native/pom.xml to /Users/mraible/.m2/repository/org/springframework/experimental/spring-graalvm-native-build/0.8.1-SNAPSHOT/spring-graalvm-native-build-0.8.1-SNAPSHOT.pom
[INFO]
[INFO] ---< org.springframework.experimental:spring-graalvm-native-feature >---
[INFO] Building Spring GraalVM Native feature 0.8.1-SNAPSHOT              [2/7]
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- build-helper-maven-plugin:3.1.0:add-source (add-json-shade-source) @ spring-graalvm-native-feature ---
[INFO] Source directory: /Users/mraible/dev/spring-graalvm-native/spring-graalvm-native-feature/src/json-shade/java added.
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ spring-graalvm-native-feature ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ spring-graalvm-native-feature ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 72 source files to /Users/mraible/dev/spring-graalvm-native/spring-graalvm-native-feature/target/classes
[INFO] /Users/mraible/dev/spring-graalvm-native/spring-graalvm-native-feature/src/main/java/org/springframework/graalvm/support/ResourcesHandler.java: Some input files use or override a deprecated API.
[INFO] /Users/mraible/dev/spring-graalvm-native/spring-graalvm-native-feature/src/main/java/org/springframework/graalvm/support/ResourcesHandler.java: Recompile with -Xlint:deprecation for details.
[INFO] /Users/mraible/dev/spring-graalvm-native/spring-graalvm-native-feature/src/main/java/org/springframework/graalvm/type/Type.java: /Users/mraible/dev/spring-graalvm-native/spring-graalvm-native-feature/src/main/java/org/springframework/graalvm/type/Type.java uses unchecked or unsafe operations.
[INFO] /Users/mraible/dev/spring-graalvm-native/spring-graalvm-native-feature/src/main/java/org/springframework/graalvm/type/Type.java: Recompile with -Xlint:unchecked for details.
[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] /Users/mraible/dev/spring-graalvm-native/spring-graalvm-native-feature/src/main/java/org/springframework/graalvm/type/TypeSystem.java:[1070,55] reference to newFileSystem is ambiguous
  both method newFileSystem(java.nio.file.Path,java.lang.ClassLoader) in java.nio.file.FileSystems and method newFileSystem(java.nio.file.Path,java.util.Map<java.lang.String,?>) in java.nio.file.FileSystems match
[INFO] 1 error
[INFO] -------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary for Spring GraalVM Native build 0.8.1-SNAPSHOT:
[INFO]
[INFO] Spring GraalVM Native build ........................ SUCCESS [  0.113 s]
[INFO] Spring GraalVM Native feature ...................... FAILURE [  1.752 s]
[INFO] Spring GraalVM Native configuration ................ SKIPPED
[INFO] Spring GraalVM Native substitutions ................ SKIPPED
[INFO] Spring GraalVM Native tools ........................ SKIPPED
[INFO] Spring GraalVM Native docs ......................... SKIPPED
[INFO] Spring GraalVM Native .............................. SKIPPED
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.067 s
[INFO] Finished at: 2020-09-21T19:35:08-06:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project spring-graalvm-native-feature: Compilation failure
[ERROR] /Users/mraible/dev/spring-graalvm-native/spring-graalvm-native-feature/src/main/java/org/springframework/graalvm/type/TypeSystem.java:[1070,55] reference to newFileSystem is ambiguous
[ERROR]   both method newFileSystem(java.nio.file.Path,java.lang.ClassLoader) in java.nio.file.FileSystems and method newFileSystem(java.nio.file.Path,java.util.Map<java.lang.String,?>) in java.nio.file.FileSystems match
[ERROR]
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
[ERROR]
[ERROR] After correcting the problems, you can resume the build with the command
[ERROR]   mvn <args> -rf :spring-graalvm-native-feature

Ha, ^^ fails because I was trying to build with Java 15. Trying Java 11 now...

Java 11 worked! Now I have spring-graalvm-native snapshots installed locally. If this build/run doesn't work, I'll try building with Java 11...

... 1 hour later ...

Spring Boot 2.4.0-M3 and spring-graalvm-native snapshots did not solve the problem.

Neither did building with Java 11. I'm stumped.

@mraible
Copy link
Contributor Author

mraible commented Sep 22, 2020

It seems I might need to pass in --enable-https as part of the build. I tried this:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <image>
            <env>
                <BP_BOOT_NATIVE_IMAGE>1</BP_BOOT_NATIVE_IMAGE>
                <BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS>
                    -Dspring.native.remove-yaml-support=true
                    -Dspring.spel.ignore=true
                    --enable-https
                </BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS>
            </env>
        </image>
    </configuration>
</plugin>

And it got me a bit further! Now I get:

2020-09-22 03:08:31.096  WARN 1 --- [           main] ConfigServletWebServerApplicationContext : 
Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: 
Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': 
Unsatisfied dependency expressed through method 'setFilterChainProxySecurityConfigurer' parameter 1; 
nested exception is org.springframework.beans.ConversionNotSupportedException: 
Failed to convert value of type 'java.lang.String' to required type 'java.util.List'; 
nested exception is java.lang.IllegalStateException: 
Cannot convert value of type 'java.lang.String' to required type 'org.springframework.security.config.annotation.SecurityConfigurer': 
no matching editors or conversion strategy found

I'll try downgrading...

Downgraded to Spring Boot 2.4.0-M2 and 0.8.0 of Spring's GraalVM support. I get the same error. Hmmmm...

Remembering that Spring Security doesn't auto-configure a resource server, I tried adding a SecurityConfiguration class:

package com.okta.rest;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(request -> request.anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.jwt());
    }
}

And that resulted in:

org.springframework.beans.factory.BeanDefinitionStoreException:
 @Configuration classes need to be marked as proxyBeanMethods=false. Found: [securityConfiguration]
        at org.springframework.context.annotation.ConfigurationClassPostProcessor.enhanceConfigurationClasses(ConfigurationClassPostProcessor.java:436) ~[com.okta.rest.Application:na]

To try and fix this, I added the following to SecurityConfiguration.java:

import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)

Nope. Now the error is:

org.springframework.beans.factory.UnsatisfiedDependencyException: 
Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': 
Unsatisfied dependency expressed through method 'setFilterChainProxySecurityConfigurer' parameter 1; nested exception is org.springframework.beans.ConversionNotSupportedException: 
Failed to convert value of type 'java.lang.String' to required type 'java.util.List'; nested exception is java.lang.IllegalStateException: 
Cannot convert value of type 'java.lang.String' to required type 'org.springframework.security.config.annotation.SecurityConfigurer': no matching editors or conversion strategy found

Next, I tried removing @EnableWebSecurity and just using @Configuration(proxyBeanMethods = false).

This does not solve the problem.

@bdemers
Copy link
Contributor

bdemers commented Sep 22, 2020

I'm not sure on the exact overlap between running graal native image directly or via the Spring plugin (i'm guessing they are similar).
But these are the options I needed to enable for the Okta CLI:
https://github.com/oktadeveloper/okta-cli/blob/master/cli/pom.xml#L205-L211

It's a much different use case though, and I remember pulling out my hair to get things working originally.
(NOTE: I'm only using a script (exec plugin) because the previous version of the Graal Native Image Maven Plugin didn't work correctly on Windows.

@mraible
Copy link
Contributor Author

mraible commented Sep 30, 2020

It looks like this will be fixed in Spring GraalVM Native 0.9.0, which is scheduled for December 18, 2020.

https://twitter.com/sdeleuze/status/1309491871769595904

@mraible
Copy link
Contributor Author

mraible commented Dec 14, 2020

From https://twitter.com/sdeleuze/status/1338508916171427841:

We've just released Spring Native for GraalVM 0.8.4 based on SpringBoot 2.4.1. It introduces SpringSecurity,
@RSocketIO, WebSocket and scheduling support. Enjoy!

@mraible
Copy link
Contributor Author

mraible commented Dec 14, 2020

I tried this again today with the Spring GraalVM Native v0.8.4 release. Here's the steps I used.

Create a new Spring Boot app using HTTPie:

http https://start.spring.io/starter.zip \
     dependencies==web,okta \
     packageName==com.okta.rest \
     name=spring-boot \
     type=maven-project \
     -o spring-boot.zip

Then, add a HelloController:

package com.okta.rest.controller;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(@AuthenticationPrincipal Principal principal) {
        return "Hello, " + principal.getName() + "!";
    }
}

Configure it to talk to Okta using the Okta CLI:

okta apps create 

Select Web > Okta Spring Boot Starter.

I configured the Spring Boot Maven plugin:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <image>
            <builder>paketobuildpacks/builder:tiny</builder>
            <env>
                <BP_BOOT_NATIVE_IMAGE>1</BP_BOOT_NATIVE_IMAGE>
                <BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS>
                    --enable-all-security-services
                </BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS>
            </env>
        </image>
    </configuration>
</plugin>

I added the Spring GraalVM dependency:

<dependency>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-graalvm-native</artifactId>
    <version>0.8.4</version>
</dependency>

And built my application.

./mvnw spring-boot:build-image

I got an error about it not being available in a repo, so I added milestone repos to my pom.xml:

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
    </repository>
</repositories>
<pluginRepositories>
    <pluginRepository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
    </pluginRepository>
</pluginRepositories>

Then I tried to build it again. It fails with a reasonable error.

[INFO]     [creator]     Excluding 103 auto-configurations from spring.factories file
[INFO]     [creator]     Processing spring.factories - EnableAutoConfiguration lists #6 configurations
[INFO]     [creator]     Fatal error:java.lang.IllegalStateException: java.lang.IllegalStateException: 
ERROR: in 'com.okta.spring.boot.oauth.OktaOAuth2AutoConfig' these methods are directly invoking methods marked 
@Bean: [oidcUserService] - due to the enforced proxyBeanMethods=false for components in a native-image, please 
consider refactoring to use instance injection. If you are confident this is not going to affect your 
application, you may turn this check off using -Dspring.native.verify=false.

Like before, I updated my configuration to avoid proxies:

@SpringBootApplication(proxyBeanMethods = false)
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

Same error, so I tried ./mvnw spring-boot:build-image -Dspring.native.verify=false. It still fails:

[INFO]     [creator]     Caused by: java.lang.IllegalStateException: 
ERROR: in 'com.okta.spring.boot.oauth.OktaOAuth2AutoConfig' these methods are directly invoking methods marked
@Bean: [oidcUserService] - due to the enforced proxyBeanMethods=false for components in a native-image, 
please consider refactoring to use instance injection. If you are confident this is not going to affect 
your application, you may turn this check off using -Dspring.native.verify=false.                                              

@mraible
Copy link
Contributor Author

mraible commented Dec 15, 2020

I tried switching to Spring Security's dependencies and configuration as a workaround:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

Change application.properties to use Spring Security's key:

spring.security.oauth2.resourceserver.jwt.issuer-uri=https://dev-133320.okta.com/oauth2/default

Then, I tried:

./mvnw spring-boot:build-image

It worked! Next, let's see if it works:

docker run -p 8080:8080 docker.io/library/demo:0.0.1-SNAPSHOT

Nope!

Caused by: java.net.MalformedURLException: Accessing an URL protocol that was not enabled. 
The URL protocol https is supported but not enabled by default. It must be enabled by adding the 
--enable-url-protocols=https option to the native-image command.

I updated my pom.xml:

<env>
    <BP_BOOT_NATIVE_IMAGE>1</BP_BOOT_NATIVE_IMAGE>
    <BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS>
        --enable-all-security-services
        --enable-url-protocols=https
    </BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS>
</env>

Now it builds and runs! 💥

I tested it and it almost works.

$ http :8080/hello # 401, generate token...
$ TOKEN=eyJraWQiOiJxOE1QMjFNNHZCVmxOSkxGbFFWNlN...
$ http :8080/hello Authorization:"Bearer $TOKEN"

This results in a 500:

HTTP/1.1 500
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: close
Content-Type: application/json
Date: Tue, 15 Dec 2020 00:03:34 GMT
Expires: 0
Pragma: no-cache
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

{
    "error": "Internal Server Error",
    "message": "",
    "path": "/hello",
    "status": 500,
    "timestamp": "2020-12-15T00:03:34.458+00:00"
}

In the Docker console, the error is:

SEVERE: Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.NullPointerException] with root cause
java.lang.NullPointerException
	at com.okta.rest.controller.HelloController.hello(HelloController.java:14)
	at java.lang.reflect.Method.invoke(Method.java:566)

@mraible
Copy link
Contributor Author

mraible commented Dec 15, 2020

Hmmm, the NullPointerException happens even when I run mvn spring-boot:run.

@bdemers Any idea why @AuthenticationPrincipal Principal principal isn't resolving?

@mraible
Copy link
Contributor Author

mraible commented Dec 15, 2020

Changing my HelloController to the following fixed the NPE when running with mvn spring-boot:run:

package com.okta.rest.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(Principal principal) {
        return "Hello, " + principal.getName() + "!";
    }
}

After I got that fixed, I re-built the image:

$ ./mvnw spring-boot:build-image
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  04:11 min

Ran it in Docker:

$ docker run -p 8080:8080 docker.io/library/demo:0.0.1-SNAPSHOT
...
2020-12-15 03:10:37.521  INFO 1 --- [           main] com.okta.rest.DemoApplication            
: Started DemoApplication in 0.978 seconds (JVM running for 0.982)

Then, in another terminal window:

$ http :8080/hello Authorization:"Bearer $TOKEN"
HTTP/1.1 200
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: keep-alive
Content-Length: 28
Content-Type: text/plain;charset=UTF-8
Date: Tue, 15 Dec 2020 03:11:03 GMT
Expires: 0
Keep-Alive: timeout=60
Pragma: no-cache
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

Hello, matt.raible@okta.com!

It works! 🎉

@mraible
Copy link
Contributor Author

mraible commented Dec 15, 2020

Next, I tried changing @SpringBootApplication(proxyBeanMethods = false) to @SpringBootApplication.

It built a few seconds faster: 04:07 min.

And it works, too! 💥

@sdeleuze
Copy link

Good news 👍🏼 For the error you see with Okta, I think you need to refactor OktaOAuth2AutoConfig to use @Configuration(proxyBeanMethods = false) (at library level in order to keep JVM compatibility and enable native support at the same time) and avoid using cross @Bean invocations as documented here. Please let me know if you need some guidance.

@mraible
Copy link
Contributor Author

mraible commented May 21, 2021

@arvindkrishnakumar-okta Any updates on this? I'd love for it to be possible to build native images for Spring Boot apps that use our starter.

@mraible
Copy link
Contributor Author

mraible commented Jun 14, 2021

I tried this again today with Spring Boot 2.5.1, Spring Native v0.10.0, and the Okta Spring Boot starter v2.0.1.

I don't expect a response from anyone on this, I just want to document the latest state of things. 🙂

Here's my pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>11</java.version>
        <spring-native.version>0.10.0</spring-native.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.okta.spring</groupId>
            <artifactId>okta-spring-boot-starter</artifactId>
            <version>2.0.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.experimental</groupId>
            <artifactId>spring-native</artifactId>
            <version>${spring-native.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <image>
                        <builder>paketobuildpacks/builder:tiny</builder>
                        <env>
                            <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
                            <BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS>
                                --enable-all-security-services
                                --enable-url-protocols=https
                            </BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS>
                        </env>
                    </image>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.experimental</groupId>
                <artifactId>spring-aot-maven-plugin</artifactId>
                <version>${spring-native.version}</version>
                <executions>
                    <execution>
                        <id>test-generate</id>
                        <goals>
                            <goal>test-generate</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>generate</id>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/release</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/release</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>

</project>

I added the following to my application.properties file:

okta.oauth2.issuer=https://dev-1309757.okta.com/oauth2/default

When I build with mvn spring-boot:build-image, the error is:

[ERROR] Failed to execute goal org.springframework.experimental:spring-aot-maven-plugin:0.10.0:test-generate 
(test-generate) on project demo: Build failed during Spring AOT test code generation: ERROR: in 'com.okta.spring.boot.oauth.OktaOAuth2AutoConfig' these methods are directly invoking methods marked @
Bean: [oidcUserService] - due to the enforced proxyBeanMethods=false for components in a native-image, 
please consider refactoring to use instance injection. If you are confident this is not going to affect your application, 
you may turn this check off using -Dspring.native.verify=false. -> [Help 1]

Next, I replaced the Okta Spring Boot starter with Spring Security:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

And changed my property name in application.properties to spring.security.oauth2.resourceserver.jwt.issuer-uri.

I also added a resource server with Spring Security.

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;

@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(request -> request.anyRequest().authenticated())
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
    }
}

Then, I built my image:

./mvnw spring-boot:build-image
...
Execution time: 3 min. 51 s. 

And ran it with:

docker run -p 8080:8080 docker.io/library/demo:0.0.1-SNAPSHOT

Everything starts up:

2021-06-14 21:37:20.668  INFO 1 --- [           main] com.okta.rest.DemoApplication            : Started DemoApplication in 0.846 seconds (JVM running for 0.849)

And a call to /hello with a valid access token works as expected.

$ http :8080/hello Authorization:"Bearer $TOKEN"
HTTP/1.1 200
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: keep-alive
Content-Length: 28
Content-Type: text/plain;charset=UTF-8
Date: Mon, 14 Jun 2021 21:38:37 GMT
Expires: 0
Keep-Alive: timeout=60
Pragma: no-cache
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

Hello, matt.raible@okta.com!

@psinghal04
Copy link

psinghal04 commented Aug 27, 2021

@mraible, the solution outlined by you above works for authentication, but not for authorization via the annotation @PreAuthorize("hasAuthority('<>')"). The behavior seems to be different when using the Okta Spring Boot Starter versus when using the spring-security-oauth2-resource-server. The same @PreAuthorize annotation works fine with Okta Spring Boot Starter, but gives a 403 with spring-security-oauth2-resource-server, the JWT access token being the same in both cases (it contains the proper "groups" attributes that I am trying to authorize against in the @PreAuthorize annotations). Do you have a suggestion on how to address this?

Note: @PreAuthorize usage is as prescribed in this blog post: https://developer.okta.com/blog/2019/06/20/spring-preauthorize

@psinghal04
Copy link

@mraible, I was able to solve the above issue with the @PreAuthorize("hasAuthority('<>')") that came up when I switched from okta-spring-boot-starter to spring-security-oauth2-resource-server per your outline (in order to get my Okta-driven Spring Boot app to compile and work with Spring Native).

The root cause seems to be that the spring-security-oauth2-resource-server converts the JWT access token differently than okta-spring-boot-starter. It was not extracting the claims in the "groups" attribute of the access token JWT. By default, it extracts the "scope" attributes for authorities, which was not matching with my "@PreAuthorize("hasAuthority('<>')") annotations, where I had used the "groups" values that I had configured for the claims in my Okta config. This was resulting in the 403.

To solve this, I added a custom JwtAuthenticationConverter as suggested in this post: https://stackoverflow.com/questions/58205510/spring-security-mapping-oauth2-claims-with-roles-to-secure-resource-server-endp. This allowed me to customize the JWT conversion logic in spring-security-oauth2-resource-server to use the "groups" attribute in the Okta JWT token to extract the claims. With this change, my "@PreAuthorize("hasAuthority('<>')") annotations now work correctly in the Spring Native build of my Okta Spring Boot app.

So, I think the instructions you have outlined, along with the above additional step should together work well to get a Spring Boot app that uses Okta to build successfully with Spring Native and function correctly with the expected authn and authz behavior.

@mraible
Copy link
Contributor Author

mraible commented Aug 27, 2021

@psinghal04 Your experience matches mine. When using Spring Security's resource-server in JHipster, we had to add a JwtDecoder bean to manage the mapping of groups to authorities.

https://github.com/jhipster/generator-jhipster/blob/main/generators/server/templates/src/main/java/package/config/SecurityConfiguration.java.ejs#L331

For oauth2Login(), a userAuthoritiesMapper() works.

https://github.com/jhipster/generator-jhipster/blob/main/generators/server/templates/src/main/java/package/config/SecurityConfiguration.java.ejs#L313

@mraible
Copy link
Contributor Author

mraible commented Sep 14, 2021

This will be fixed by v2.1.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants