Recently, I have been studying in a closed door, but the process is still a little hard. Fortunately, there are also teachers who take the excellent courses with them, so they have not taken many detours. I haven't updated this article for a long time. What I want to share with you is to learn how to build a secure SPA using Spring Boot and OAuth at the same time of using the Spring Boot starter program, so as to obtain other support for authentication and permission mapping.
Even the most basic JavaScript single page application (SPA) is likely to need to access resources safely from the source application, and if you are a Java developer like me, it may be a Spring Boot application, and you may want to use OAuth 2.0 implicit flow. Through this process, your client will send a hosted token in each request, and your server-side application will use an identity provider (IdP) to validate the token.
In this tutorial, you'll learn more about implicit processes by building two small applications that demonstrate these principles: a simple SPA client application with a little JQuery and a back-end service with Spring Boot. You will start by using the standard Spring OAuth bit, then switch to Okta Spring Boot Starter and check what it adds. The previous sections will have nothing to do with suppliers, but since I don't know nothing, I'll show you how to use Okta as your IdP.
Create a Spring Boot application
If you haven't tried start.spring.io yet, click now to check With a few clicks, it will provide you with a basic, runnable Spring Boot application.
1 curl https://start.spring.io/starter.tgz \ 2 -d artifactId=oauth-implicit-example \ 3 -d dependencies=security,web \ 4 -d language=java \ 5 -d type=maven-project \ 6 -d baseDir=oauth-implicit-example \ 7 | tar -xzvf -
If you want to download a project from your browser, go to: start.spring.io search and select the security dependency, then click the green Generate Project button.
After extracting the project, you should be able to start it on the command line:. / mvnw spring boot: run. The application is not yet able to perform any operations, but this is a "so far so good" check. Terminate the process with ^ C, let's start to actually write code!
Write some code!
OK, almost. First, add the Spring OAuth 2.0 dependency to pom.xml
1 <dependency> 2 <groupId>org.springframework.security.oauth</groupId> 3 <artifactId>spring-security-oauth2</artifactId> 4 <version>2.2.0.RELEASE</version> 5 </dependency>
Open DemoApplication.java. If you follow (and you are right?), it should be in src/main/java/com/example/oauthimplicitexample. It's not hard to see that the project contains only two Java classes, one of which is a test.
Annotate the class with @ EnableResourceServer, which tells Spring Security to add the necessary filters and logic to handle the OAuth implicit request.
Next, add a controller:
1 @RestController 2 public class MessageOfTheDayController { 3 @GetMapping("/mod") 4 public String getMessageOfTheDay(Principal principal) { 5 return "The message of the day is boring for user: " + principal.getName(); 6 } 7 }
That's right! Basically Hello World. Use. / mvnw spring boot: run to start your application backup. You should be able to visit http://localhost:8080/mod:
1 curl -v http://localhost:8080/mod 2 HTTP/1.1 401 3 Content-Type: application/json;charset=UTF-8 4 WWW-Authenticate: Bearer realm="oauth2-resource", error="unauthorized", error_description="Full authentication is required to access this resource" 5 { 6 "error": "unauthorized", 7 "error_description": "Full authentication is required to access this resource" 8 }
401? Yes, it is safe by default! In addition, we did not actually provide any configuration details for OAuth IdP. Use ^ C to stop the server and move to the next section.
Get your OAuth information ready
As mentioned above, you will continue to use Okta. You can be there. https://developer.okta.com/ Sign up for a free (permanent) account on. Just click the register button and fill out the form. After you do this, you will get two things, the Okta basic URL, which looks like: dev-123456.oktapreview.com And an email with instructions on how to activate your account.
Activate your account, and when you are still in the Okta developer console, the final step is to create an Okta SPA application. On the top menu bar, click Applications, and then click Add Application. Select SPA, and then click Next.
Fill in the form with the following values:
- Name: OAuth Implicit Tutorial
- Base URIs: http://localhost:8080/
- Login redirect URIs: http://localhost:8080/
Leave everything else as the default, and then click Done. At the bottom of the next page is your customer ID, which you will use in the next step.
Configure OAuth for Spring
The generated sample application uses the application.properties file. I prefer YAML, so I rename the file application.yml.
The application resource server only needs to know how to validate the access token. Because the format of the access token is not defined by OAuth 2.0 or the OIDC specification, the token can be verified remotely. The generated sample application uses
1 security: 2 oauth2: 3 resource: 4 userInfoUri: https://dev-123456.oktapreview.com/oauth2/default/v1/userinfo
At this point, you can start the application and start validating the access token! But, of course, you will need an access token to authenticate
Create login page
For convenience, you will reuse your existing Spring Boot application to host SPA. Typically, these assets can be hosted elsewhere: another application, CDN, etc. For the purposes of this tutorial, hosting a lonely index.html file in another application seems a bit excessive.
Create a new file src/main/resources/static/index.html and fill it with the following:
1 <!doctype html> 2 <html lang="en"> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 6 <meta name="description" content=""> 7 <meta name="author" content=""> 8 <title>Okta Implicit Spring-Boot</title> 9 <base href="/"> 10 <script src="https://ok1static.oktacdn.com/assets/js/sdk/okta-signin-widget/2.3.0/js/okta-sign-in.min.js" type="text/javascript"></script> 11 <link href="https://ok1static.oktacdn.com/assets/js/sdk/okta-signin-widget/2.3.0/css/okta-sign-in.min.css" type="text/css" rel="stylesheet"> 12 <link href="https://ok1static.oktacdn.com/assets/js/sdk/okta-signin-widget/2.3.0/css/okta-theme.css" type="text/css" rel="stylesheet"> 13 <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> 14 <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> 15 <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> 16 </head> 17 <body> 18 <!-- Render the login widget here --> 19 <div id="okta-login-container"></div> 20 <!-- Render the REST response here --> 21 <div id="cool-stuff-here"></div> 22 <!-- And a logout button, hidden by default --> 23 <button id="logout" type="button" style="display:none">Logout</button> 24 <script> 25 $.ajax({ 26 url: "/sign-in-widget-config", 27 }).then(function(data) { 28 // we are priming our config object with data retrieved from the server in order to make this example easier to run 29 // You could statically define your config like if you wanted too: 30 /* 31 const data = { 32 baseUrl: 'https://dev-123456.oktapreview.com', 33 clientId: '00icu81200icu812w0h7', 34 redirectUri: 'http://localhost:8080', 35 authParams: { 36 issuer: 'https://dev-123456.oktapreview.com/oauth2/default', 37 responseType: ['id_token', 'token'] 38 } 39 }; */ 40 // we want the access token so include 'token' 41 data.authParams.responseType = ['id_token', 'token']; 42 data.authParams.scopes = ['openid', 'email', 'profile']; 43 data.redirectUri = window.location.href; // simple single page app 44 // setup the widget 45 window.oktaSignIn = new OktaSignIn(data); 46 // handle the rest of the page 47 doInit(); 48 }); 49 /** 50 * Makes a request to a REST resource and displays a simple message to the page. 51 * @param accessToken The access token used for the auth header 52 */ 53 function doAllTheThings(accessToken) { 54 // include the Bearer token in the request 55 $.ajax({ 56 url: "/mod", 57 headers: { 58 'Authorization': "Bearer " + accessToken 59 }, 60 }).then(function(data) { 61 // Render the message of the day 62 $('#cool-stuff-here').append("<strong>Message of the Day:</strong> "+ data); 63 }) 64 .fail(function(data) { 65 // handle any errors 66 console.error("ERROR!!"); 67 console.log(data.responseJSON.error); 68 console.log(data.responseJSON.error_description); 69 }); 70 // show the logout button 71 $( "#logout" )[0].style.display = 'block'; 72 } 73 function doInit() { 74 $( "#logout" ).click(function() { 75 oktaSignIn.signOut(() => { 76 oktaSignIn.tokenManager.clear(); 77 location.reload(); 78 }); 79 }); 80 // Check if we already have an access token 81 const token = oktaSignIn.tokenManager.get('my_access_token'); 82 // if we do great, just go with it! 83 if (token) { 84 doAllTheThings(token.accessToken) 85 } else { 86 // otherwise show the login widget 87 oktaSignIn.renderEl( 88 , 89 function (response) { 90 // check if success 91 if (response.status === 'SUCCESS') { 92 // for our example we have the id token and the access token 93 oktaSignIn.tokenManager.add('my_id_token', response[0]); 94 oktaSignIn.tokenManager.add('my_access_token', response[1]); 95 // hide the widget 96 oktaSignIn.hide(); 97 // now for the fun part! 98 doAllTheThings(response[1].accessToken); 99 } 100 }, 101 function (err) { 102 // handle any errors 103 console.log(err); 104 } 105 ); 106 } 107 } 108 </script> 109 </body> 110 </html>
This page does the following:
- Show Okta login widget and get access token
- Call the / sign in widget config controller to configure the widget (let's assume this file is provided by another service)
- After the user logs in, the page calls the / mod controller (with access token) and displays the result
To support our HTML, we need to create a new Controller for the / sign in widget config endpoint.
In the same package as the Spring Boot Application class, create a new SignInWidgetConfigControllerclass class:
1 @RestController 2 public class SignInWidgetConfigController { 3 private final String issuerUrl; 4 private final String clientId; 5 public SignInWidgetConfigController(@Value("#{@environment['okta.oauth2.clientId']}") String clientId, 6 @Value("#{@environment['okta.oauth2.issuer']}") String issuerUrl) { 7 Assert.notNull(clientId, "Property 'okta.oauth2.clientId' is required."); 8 Assert.notNull(issuerUrl, "Property 'okta.oauth2.issuer' is required."); 9 this.clientId = clientId; 10 this.issuerUrl = issuerUrl; 11 } 12 @GetMapping("/sign-in-widget-config") 13 public WidgetConfig getWidgetConfig() { 14 return new WidgetConfig(issuerUrl, clientId); 15 } 16 public static class WidgetConfig { 17 public String baseUrl; 18 public String clientId; 19 public Map<String, Object> authParams = new LinkedHashMap<>(); 20 WidgetConfig(String issuer, String clientId) { 21 this.clientId = clientId; 22 this.authParams.put("issuer", issuer); 23 this.baseUrl = issuer.replaceAll("/oauth2/.*", ""); 24 } 25 } 26 }
Add the corresponding configuration to the application.yml file:
1 okta: 2 oauth2: 3 # Client ID from above step 4 clientId: 00ICU81200ICU812 5 issuer: https://dev-123456.oktapreview.com/oauth2/default
The last thing is to allow the public access to the index.html page and / sign in widget config
Define ResourceServerConfigurerAdapter in your application to allow access to these resources.
1 @Bean 2 protected ResourceServerConfigurerAdapter resourceServerConfigurerAdapter() { 3 return new ResourceServerConfigurerAdapter() { 4 @Override 5 public void configure(HttpSecurity http) throws Exception { 6 http.authorizeRequests() 7 .antMatchers("/", "/index.html", "/sign-in-widget-config").permitAll() 8 .anyRequest().authenticated(); 9 } 10 }; 11 }
Move!
Use. / mvnw spring boot: run to start your application again, and browse to http://localhost:8080/ . You should be able to sign in with a new Okta account and see the messages for the day.
Try Okta Spring Boot Starter
You've been using Spring Security OAuth 2.0's out of the box support so far (except for the login page). This is because: standard! There are some problems in this method:
- Every request to our application requires an unnecessary round trip back to OAuth IdP
- We don't know what scope was used to create the access token
- In this case, the user's group / role is not available
These may not be a problem for your application, but fixing them is as simple as adding another dependency to your POM file:
1 <dependency> 2 <groupId>com.okta.spring</groupId> 3 <artifactId>okta-spring-boot-starter</artifactId> 4 <version>0.2.0</version> 5 </dependency>
If necessary, you can even reduce the application.yml file, where any security. * attribute takes precedence over the okta. * attribute:
1 okta: 2 oauth2: 3 clientId: 00ICU81200ICU812 4 issuer: https://dev-123456.oktapreview.com/oauth2/default
Restarting your application has solved the first two problems!
The last one requires additional steps, and you will have to add additional data to Okta's access token:
Go back to the Okta Developer Console and on the menu bar, click API > Authorization Server. In this example, we have been using the "default" Authorization Server, so click "edit" and select the "Claims" tab. Click Add Claim and fill out the form with the following values:
- Name: groups
- Include in token type: Access Token
- Value type: Groups
- Filter: Regex - .*
Leave the rest as the default, then click Create.
OKTA spring boot starter automatically maps the values in the group declaration to Spring Security Authority. In the standard Spring Security way, we can annotate our methods to configure access levels.
To enable the @ PreAuthorize annotation, you need to add @ EnableGlobalMethodSecurity to the Spring Boot Application. If you also want to verify the OAuth scope, you need to add the OAuth2MethodSecurityExpressionHandler. Just put the following code snippet into your Spring Boot Application.
1 @EnableGlobalMethodSecurity(prePostEnabled = true) 2 protected static class GlobalSecurityConfiguration extends GlobalMethodSecurityConfiguration { 3 @Override 4 protected MethodSecurityExpressionHandler createExpressionHandler() { 5 return new OAuth2MethodSecurityExpressionHandler(); 6 } 7 }
Finally, use @ PreAuthorize to update the MessageOfTheDayController (in this case, you allow anyone in the "everyone" or "email" scope).
1 @RestController 2 public class MessageOfTheDayController { 3 @GetMapping("/mod") 4 @PreAuthorize("hasAuthority('Everyone') || #oauth2.hasScope('email')") 5 public String getMessageOfTheDay(Principal principal) { 6 return "The message of the day is boring for user: " + principal.getName(); 7 } 8 }
Thank you for reading~