Last year, Micah Silverman wrote about integrating Spring Boot, Spring Security, and Stormpath. Today, I’m going to take you on a similar journey, except this time you’ll be using AngularJS and Stormpath’s AngularJS SDK for the UI. Along the way, you’ll learn how to create REST endpoints with Spring Data REST, configure Spring Boot to handle cross-domain requests, and use Stormpath to make authentication a breeze.
Get Started with AngularJS
To get started with AngularJS, I used to recommend cloning Angular’s angular-seed project. I wrote about developing a simple Angular app with angular-seed early last year. I followed that up with an article on testing your Angular app.
As part of a recent update to the JHipster mini-book, I created my own my angular-seed fork that uses UI-Router, Gulp, and Browsersync. This project will be the starting point for this tutorial. This tutorial assumes you have Node.js, Git and Gulp installed.
To begin, clone my fork of the angular-seed project and install all its dependencies:
git clone https://github.com/mraible/angular-seed.git angularjs-spring-boot-stormpath cd angularjs-spring-boot-stormpath npm i gulp
TIP: You can also use Facebook’s yarn as an alternative to npm. It caches downloads and can be up to 11 times faster with a warm cache. I tried it on this project and found it took 20.63 seconds the first time I ran “yarn install”. The second time, it took 0.14 seconds! You can install yarn with npm using npm -g install yarn.
After you’ve performed these steps, your browser should launch and you should see a screen like this:
You can modify one of its CSS files (e.g. app/css/app2.css) to add some body padding:
body {
padding: 10px;
}
When you save the file, Browsersync will auto-reload your changes and things look a bit better.
Angular has a $resource service that allows you to easily make HTTP calls to a REST endpoint. In the following section, you’ll use $resource, and a JSON file to create an application that allows searching and editing. After that, you’ll change to use Spring Boot for that endpoint. Finally, we’ll integrate Stormpath to provide the following:
- Registration
- Login
- Logout
- Forgot Password
- Only allow admins to search
Add a Search Feature
The AngularJS code below might look a bit different than you’re used to. This code uses John Papa’s Angular 1 Style Guide. This guide promotes an opinionated way to write AngularJS 1.x applications; allowing you to worry less about syntax and naming conventions and more about your code.
To create a new feature, you’ll need a few different files:
search.service.jsto interact with your datasearch.controller.jsto contain your controller logicsearch.htmlto display your rendered datasearch.state.jsto contain routing information
Create app/search/search.service.js and populate it with the following JavaScript:
(function () {
'use strict';
angular
.module('myApp')
.factory('SearchService', SearchService);
SearchService.$inject = ['$resource'];
function SearchService($resource) {
return $resource('/api/search/people.json');
}
})();
The $resource service is not included by default in angular-seed. To add it to your project, add "angular-resource": "~1.5.0" to your dependencies in bower.json, or run the following command:
bower install angular-resource --save
To activate this service in your application, add a link to angular-resource.js in app/index.html.
And reference it as a dependency in app/app.js.
angular.module('myApp', [
'ui.router',
'ngResource',
'myApp.view1',
...
In addition to using $resource, SearchService reads from a people.json file. Create app/api/search/people.json with the following contents:
[
{
"id": 1,
"name": "Peyton Manning",
"phone": "(303) 567-8910",
"address": {
"street": "1234 Main Street",
"city": "Greenwood Village",
"state": "CO",
"zip": "80111"
}
},
{
"id": 2,
"name": "Demaryius Thomas",
"phone": "(720) 213-9876",
"address": {
"street": "5555 Marion Street",
"city": "Denver",
"state": "CO",
"zip": "80202"
}
},
{
"id": 3,
"name": "Von Miller",
"phone": "(917) 323-2333",
"address": {
"street": "14 Mountain Way",
"city": "Vail",
"state": "CO",
"zip": "81657"
}
}
]
Now that the data and service are in place create app/search/search.html to search and display data.
| Name | Phone | Address |
|---|---|---|
| {{person.name}} | {{person.phone}} | {{person.address.street}} {{person.address.city}}, {{person.address.state}} {{person.address.zip}} |
In this file, there are two “vm” (a.k.a. view-model) variables that the template expects in its controller: vm.term and vm.searchResults. Create app/search/search.controller.js to expose these variables:
(function () {
'use strict';
angular
.module('myApp')
.controller('SearchController', SearchController);
SearchController.$inject = ['SearchService'];
function SearchController(SearchService) {
var vm = this;
vm.search = function () {
SearchService.query(vm.term, function (response) {
var results = response.filter(function (item) {
return JSON.stringify(item).toLowerCase().includes(vm.term.toLowerCase());
});
vm.searchResults = results;
});
};
}
})();
To make things look better, you can add some new CSS rules to app/css/app2.css:
table {
margin-top: 10px;
border-collapse: collapse;
width: 100%;
}
th {
text-align: left;
border-bottom: 2px solid #ddd;
padding: 8px;
}
td {
border-top: 1px solid #ddd;
padding: 8px;
}
Add a link to the search feature in app/index.html by adding a “search” menu item:
To make this link work, you need to create a search.state.js file in the search directory that configures the “search” state.
(function () {
'use strict';
angular.module('myApp')
.config(stateConfig);
stateConfig.$inject = ['$stateProvider'];
function stateConfig($stateProvider) {
$stateProvider
.state('search', {
url: '/search',
templateUrl: 'search/search.html',
controller: 'SearchController',
controllerAs: 'vm'
});
}
})();
The final step to enabling search is making sure all the JavaScript files are referenced in app/index.html:
At this point, you should be able to run “gulp”, click on the search link and perform a search. For example, below is a screenshot after searching for “Von”.
If you’ve made it this far, congratulations! If you encountered issues along the way, see the add search feature commit to see what’s changed since cloning the original angular-seed project.
Add an Edit Feature
For the edit feature, you’ll create similar files to what you did for the search feature.
edit.state.jsto contain routing informationedit.controller.jsto contain your controller logicedit.htmlto display your rendered data
You’ll reuse the search.service.js and add a new fetch function for retrieving a single record.
Search.fetch = function (id, callback) {
Search.query(function (response) {
var results = response.filter(function (item) {
return item.id === parseInt(id);
});
return callback(results[0]);
});
};
Create app/edit/edit.state.js to route “/edit/{id}” URL to the EditController.
(function () {
'use strict';
angular.module('myApp')
.config(stateConfig);
stateConfig.$inject = ['$stateProvider'];
function stateConfig($stateProvider) {
$stateProvider
.state('edit', {
url: '/edit/:id',
templateUrl: 'edit/edit.html',
controller: 'EditController',
controllerAs: 'vm'
});
}
})();
Create app/edit/edit.controller.js and create a simple controller that fetches a person’s record by the passed in identifier.
(function () {
'use strict';
angular
.module('myApp')
.controller('EditController', EditController);
EditController.$inject = ['SearchService', '$stateParams'];
function EditController(SearchService, $stateParams) {
var vm = this;
SearchService.fetch($stateParams.id, function (response) {
vm.person = response;
});
}
})();
Add app/edit/edit.html and populate it with the following HTML to display a person’s information.
Add a link to the edit state from app/search/search.html:
{{person.name}} {{person.phone}} And add a link to the new JavaScript files you added in
app/index.html:TIP: If you get tired of adding
<script>tags in yourindex.html, you can use gulp-inject to add new files automatically.Finally, add some CSS in
app/css/app2.cssto make the form look a bit better.form { line-height: 2; } address { font-style: normal; }After making all these changes, you should be able to search for a person, click on their name and view their information.
Get Started with Spring Boot and Stormpath
Spring Initializr is a project that makes it super easy to get started with Spring Boot. It’s deployed at https://start.spring.io by default and Stormpath has an instance deployed at http://start.stormpath.io. Our instance has Stormpath Spring Boot starters available, and they should be available soon on the default instance.
To create an application with Spring Boot and Stormpath, go to http://start.stormpath.io and select the following dependencies: Web, JPA, H2, Stormpath Default, and DevTools. DevTools is a handy plugin for Spring Boot that allows you to hot-reload the application when you recompile any Java files.
Click “Generate Project” and download the resulting demo.zip file. Expand the file and copy its contents into the Angular project you created (e.g. angularjs-spring-boot-stormpath).
Because you’ve integrated Stormpath in this project, you’ll need a Stormpath account and API Keys to start the application. If you don’t have an account, go to https://api.stormpath.com/register and sign up. A developer account is free, with up to 10K API calls per month.
You’ll receive an email to activate your account. Click on the activation link and login to your account.
Click on the “Create API Key” button and copy the resulting file to
~/.stormpath/apiKey.properties.At this point, you should be able to start the Spring Boot app by running
mvn spring-boot:run. When you open http://localhost:8080 in your browser, you’ll be prompted to login using basic authentication. You can turn off basic authentication if you want (usingsecurity.basic.enabled = falseinsrc/main/resources/application.properties), but you can also integrate Stormpath with Spring Security instead.To add Stormpath support to Spring Security, create
src/main/java/com/example/SecurityConfiguration.javawith the following code.package com.example; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import static com.stormpath.spring.config.StormpathWebSecurityConfigurer.stormpath; @Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.apply(stormpath()); } }After recompiling this class and waiting for your application to reload, go to http://localhost:8080. You’ll be prompted to log in with Stormpath’s login form.
You won’t be able to log in because you haven’t created any accounts in your default application. Click on “Create Account” to create a new account. You can use the same email address as you did when you registered.
After completing registration, you should be able to log in using the email and password you entered.
NOTE: The above steps are covered in Stormpath’s Spring Boot Quickstart Guide.
You’ll likely see a 404 page from Spring Boot after you’ve successfully logged in.
This is because Stormpath redirects to / by default. To make it so you don’t see an error, create a homepage at
src/main/resources/static/index.htmlwith the following HTML:Hello World
The auto-discovery of HTML files in this directory is covered in Spring Boot’s static content documentation. If you want to make your homepage dynamic, you have to create a Controller that serves up a Thymleaf template. Thymeleaf is enabled by default when using the Stormpath Spring Boot starter.
To see what this looks like, create
src/main/java/com/example/HomeController.javawith the following code.package com.example; import com.stormpath.sdk.account.Account; import com.stormpath.sdk.servlet.account.AccountResolver; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletRequest; @Controller public class HomeController { @RequestMapping("/") public String home(HttpServletRequest request, Model model) { String name = "World"; Account account = AccountResolver.INSTANCE.getAccount(request); if (account != null) { name = account.getGivenName(); model.addAttribute(account); } model.addAttribute("name", name); return "index"; } }Then create
src/main/templates/index.htmlto say hello to the user with their name.If you compile
HelloControllerand DevTools reloads your application, you’ll likely see the following error:org.thymeleaf.exceptions.TemplateInputException: Error resolving template "index", template might not exist or might not be accessible by any of the configured Template ResolversRestarting your application manually will solve this issue, but a better solution is to configure the Spring Boot Maven plugin to add resources.
org.springframework.boot spring-boot-maven-plugin true This setting will allow you to modify templates and see the changes immediately without restarting your server.
If you’re still logged in, you should be able to refresh your browser and see a page like the one below.
You might notice that clicking the “Logout” button will prompt you to log in again. If you’d prefer to show the homepage and allow users to click a link to log in, you can allow this in your Spring Security configuration. Simply update
SecurityConfiguration.javato have the following. While you’re at it, add protection for the API endpoints you’re about to create.@Override protected void configure(HttpSecurity http) throws Exception { http.apply(stormpath()).and() .authorizeRequests() .antMatchers("/api/**").fullyAuthenticated() .antMatchers("/**").permitAll(); }For more information on the features that Stormpath provides for Spring Boot and Spring Security, see A Simple Web App with Spring Boot, Spring Security, and Stormpath – in 15 Minutes.
Add an API Endpoint
To add an API for /people, you’re going to need some data first. Spring Boot’s Spring Data JPA provides an easy way to do this. You’ll need a JPA entity to represent your data, so create
src/main/java/com/example/Person.java.package com.example; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity public class Person { private Long id; private String name; private String phone; private Address address; @Id @GeneratedValue public Long getId() { return id; } public void setId(Long id) { this.id = id; } // getters and setters removed for brevity @Override public String toString() { return "Person{" + "id=" + id + ", name='" + name + '\'' + ", phone='" + phone + '\'' + ", address=" + address + '}'; } }Create an
Address.javaclass in the same directory.package com.example; import javax.persistence.Embeddable; @Embeddable public class Address { private String street; private String city; private String state; private String zip; // getters and setters removed for brevity @Override public String toString() { return "Address{" + "street='" + street + '\'' + ", city='" + city + '\'' + ", state='" + state + '\'' + ", zip='" + zip + '\'' + '}'; } }Then create
src/main/resources/data.sqlto create sample data on startup.insert into person (name, phone, street, city, state, zip) values ('Peyton Manning', '(303) 567-8910', '1234 Main Street', 'Greenwood Village', 'CO', '80111'); insert into person (name, phone, street, city, state, zip) values ('Damaryius Thomas', '(720) 213-9876', '5555 Marion Street', 'Denver', 'CO', '80202'); insert into person (name, phone, street, city, state, zip) values ('Von Miller', '(917) 323-2333', '14 Mountain Way', 'Vail', 'CO', '81657');Spring Data JPA provides JPA repositories that make it easy to CRUD an entity. The Spring Data REST project provides support for creating JPA repositories and exposing them as REST endpoints.
Add the Spring Boot starter for Spring Data REST to your
pom.xml.org.springframework.boot spring-boot-starter-data-rest Create a
PersonRepository.javafile insrc/main/java/com/examplethat utilizes Spring Data REST.package com.example; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.springframework.data.rest.core.annotation.RepositoryRestResource; import java.util.List; @RepositoryRestResource(collectionResourceRel = "people", path = "people") public interface PersonRepository extends PagingAndSortingRepository{ List findByName(@Param("name") String name); } To make all Spring Data REST endpoints have an /api prefix, add the following to
src/main/resources/application.properties.spring.data.rest.basePath=/apiUsing HTTPie, you should be able to login via the command line using:
http -f POST localhost:8080/login login=mraible+stormpath@gmail.com password=To see the data at /api/people, copy the access token from the result of the command above and use it in an Authorization header.
http localhost:8080/api/people Authorization:'Bearer' You can also login with your browser and navigate to http://localhost:8080/api/people.
In this example, there’s a
PersonRespository#findByNamemethod that allows you to search by a person’s full name. Below is an example call to this REST endpoint.http://localhost:8080/api/people/search/findByName?name=Von%20Miller
You’ll notice this doesn’t provide full-text searching. Spring Data Elasticsearch is a good place to start if you’re looking for full-text searching.
Integrate AngularJS with Spring Boot
To integrate the AngularJS with Spring Boot, let’s first turn off Stormpath so you can access api/people without logging in. Modify
SecurityConfiguration.javato remove the stormpath() hook and allow all requests.protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/**").permitAll(); }Next, modify
app/search/search.service.jsto talk to http://localhost:8080/api/people, configure the ‘query’ function to not expect an array, and change the search function to handle the new data structure.function SearchService($resource) { var Search = $resource('http://localhost:8080/api/people', {}, { 'query': {isArray: false} }); Search.search = function (term, callback) { if (term == undefined) { // handle empty search term term = ''; } Search.query(function (response) { var people = response._embedded.people; var results = people.filter(function (item) { return JSON.stringify(item).toLowerCase().includes(term.toLowerCase()); }); return callback(results); }); }; // Search.fetch function return Search; }Fire up your Angular app using
gulp, then try its search feature. You’ll see an error in your console when you try to search. This happens because of your browser’s same-origin policy.Spring Boot supports Cross-Origin Resource Sharing (CORS) to help solve this issue. However, it only works for Spring MVC, not Spring Data REST (see DATAREST-573 for more information). The good news is the Spring Framework provides a CorsFilter you can use for filter-based frameworks. Add the following to
SpringConfiguration.java.@Bean public FilterRegistrationBean corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("http://localhost:3000"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); bean.setOrder(0); return bean; }After making these changes and waiting for your application to reload, you should be able to search people, just like you did before.
Enable Stormpath
To make everything work with Stormpath enabled, there’re several things that need to happen:
- Re-enable Stormpath in SecurityConfiguration.java
- Add Stormpath’s AngularJS SDK to the Angular app
- Create pages and controllers for Login, Logout, Registration, and Forgot Password
Revert the changes you made in
SecurityConfiguration.javato allow all requests.@Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.apply(stormpath()).and() .authorizeRequests() .antMatchers("/api/**").fullyAuthenticated() .antMatchers("/**").permitAll(); } }To add Stormpath’s AngularJS SDK to your project, you can use Bower.
bower install stormpath-sdk-angularjs --saveAdd these new files to
app/index.html:Open
app/app.jsand add these two Stormpath dependencies at the bottom of the list.angular.module('myApp', [ 'ui.router', 'ngResource', 'myApp.view1', 'myApp.view2', 'myApp.version', 'stormpath', 'stormpath.templates' ]).Modify the config block in this file to configure Stormpath to point to Spring Boot on http://localhost:8080.
config(['$stateProvider', '$urlRouterProvider', 'STORMPATH_CONFIG', function($stateProvider, $urlRouterProvider, STORMPATH_CONFIG) { // For any unmatched url, redirect to /view1 $urlRouterProvider.otherwise('/view1'); STORMPATH_CONFIG.ENDPOINT_PREFIX = 'http://localhost:8080'; }]).In the same
app.jsfile, add a run block, place it below the config block (make sure you move the semicolon from the config block to the run block):run(['$stormpath', function($stormpath){ $stormpath.uiRouter({ loginState: 'login', defaultPostLoginState: 'view1' }]); });This configures Stormpath to do the following:
- Redirect users to the login view if they try to access a restricted view. After logging in, they are sent back to the view that they originally requested.
- Send users to the view1 view after login if they have visited the login page directly (they did not try to access a restricted view first).
Modify
app/index.htmlto adjust the menu so only logged in users can access view2 and the search feature. This code makes use of theif-userandif-not-userdirectives, which are documented in the AngularJS SDK docs.NOTE: There is an issue in the Stormpath’s Java SDK where if-user-in-group=”ADMIN” doesn’t work. A fix will be available in a future release. In the meantime, there’s a workaround in Stormpath’s AngularJS SDK.
Since no “login” state exists yet, you’ll need to create one. Create
app/login/login.state.jsand populate it with the following.(function () { 'use strict'; angular.module('myApp') .config(stateConfig); stateConfig.$inject = ['$stateProvider']; function stateConfig($stateProvider) { $stateProvider .state('login', { url: '/login', templateUrl: 'login/login.html' }); } })();No controller is needed because Stormpath provides the Controller for you. Create
app/login/login.htmland use the spLoginForm directive to render the login form.Add
login.state.jstoindex.html.At this point, I noticed my console said I was not allowed to access http://localhost:8080/me because my origin (http://localhost:3000/me) was not supported. After debugging, I figured out this was because the main
StormpathFiltercomes before theCorsFilter. To fix this, add the following toapplication.properties.stormpath.web.stormpathFilter.order=1After making this change, you should be able to hit http://localhost:3000 and click the Login link.
It doesn’t look great by default, but that’s because the default template expects Bootstrap to be one of the CSS files. Add Bootstrap with Bower (
bower install bootstrap --save), add a<link>inapp/index.html, and the form becomes a lot prettier.You should be able to login with the username and password combination you registered with earlier.
NOTE: There’s an issue when trying to logout cross-domain with the Stormpath’s Spring Boot integration. We hope to have a fix soon. In the meantime, refreshing your browser after clicking the Logout link will show you have successfully logged out.
Unlike the Spring Boot Thymeleaf templates, the AngularJS
spLoginFormdirective does not include a link to register. You can add this above the form by modifyingapp/login/login.htmland adding the following HTML at the top.Login or Create Account
After making this change, the login form looks more like the Thymeleaf version.
However, the “Create Account” link won’t work since there is no “register” state yet. To create it, add
app/register/register.state.js:(function () { 'use strict'; angular.module('myApp') .config(stateConfig); stateConfig.$inject = ['$stateProvider']; function stateConfig($stateProvider) { $stateProvider .state('register', { url: '/register', templateUrl: 'register/register.html' }); } })();Create a
register.htmlfile in the same directory.Create Account
TIP: Wrapping the template in a
.container-fluidclass removes the horizontal scrolling you can see in the previous screenshot.Add a reference to
register.state.jsinapp/index.html.Now clicking on the “Create Account” link should render a registration form.
When I first tried to register using this form, I received a strange error.
Unable to invoke Stormpath controller: Unable to read JSON value from request body: No content to map due to end-of-input at [Source: org.apache.catalina.connector. CoyoteInputStream@19b6af88; line: 1, column: 0]To fix this, I had to modify
app/app.jsand set the form content type toapplication/json.STORMPATH_CONFIG.ENDPOINT_PREFIX = 'http://localhost:8080'; STORMPATH_CONFIG.FORM_CONTENT_TYPE = 'application/json';There is an open issue to remove this extra step in Stormpath’s AngularJS SDK.
The last step is to configure the state for the Forgot Password feature. Create
app/forgot/forgot.state.jsand populate it with the following code.(function () { 'use strict'; angular.module('myApp') .config(stateConfig); stateConfig.$inject = ['$stateProvider']; function stateConfig($stateProvider) { $stateProvider .state('forgot', { url: '/forgot', templateUrl: 'forgot/forgot.html' }); } })();Create
forgot.htmlin the same directory and use the spPasswordResetRequestForm directive in it.Forgot your password?
Don’t forget to add a reference to this new state in
app/index.html.Now, you should be able to click the Forgot Password link in the Login form. However, the current release of Stormpath’s AngularJS SDK doesn’t support Angular’s hashbang mode and expects HTML5 mode. The Forgot Password link doesn’t work in hashbang mode.
The easy way to workaround this is to pass in a
$locationProviderto the config block inapp/app.jsand set HTML5 mode to true.$locationProvider.html5Mode(true);You’ll also need to add
<base href="/">to the<head>ofapp/index.htmlto make HTML5 mode work. This does cause issues with Browsersync reloading; I haven’t figured out a solution for that yet.If you don’t want to use HTML5 mode in your AngularJS application, you can override the templates to provide relative links. For example, you can change
login.htmlto point to your customized template.Then you can create
app/stormpath/login.tpland tweak the default template so the registration link uses UI Router’sui-srefdirective.Forgot PasswordBoth methods will allow you to navigate to the “forgot” state when clicking on the “Forgot Password” link.
Summary
I hope you’ve enjoyed this tour of AngularJS, Spring Boot, and Stormpath. I showed you how to do a number of things in this article:
- Create a simple AngularJS application with a search feature
- Create a Spring Boot application with Stormpath integrated
- Communicate between the AngularJS and Spring Boot apps cross-domain
- Add login, registration, and forgot password features to the AngularJS application
The source code for the completed application referenced here is available at https://github.com/stormpath/angularjs-spring-boot-stormpath-example.
In future posts, I’ll talk about how the Stormpath’s AngularJS SDK can be used to prevent access to certain states and hide links to different groups. I also plan to show you how to integrate Stormpath into JHipster.
As far as Stormpath’s Angular 2 support—we’re working on it! We hope to have something for you to experiment with by Thanksgiving (November 24).
If you have any questions, don’t hesitate to leave a comment or hit me up on Twitter at @mraible.
References
The good Dr. Dave Syer wrote an excellent blog series in 2015 on integrating AngularJS with Spring Boot and Spring Security.
- Spring and Angular JS: A Secure Single Page Application
- The Login Page: Angular JS and Spring Security Part II
- The Resource Server: Angular JS and Spring Security Part III
- The API Gateway Pattern: Angular JS and Spring Security Part IV
- SSO with OAuth2: Angular JS and Spring Security Part V
The post Tutorial: Get Started with AngularJS, Spring Boot, and Stormpath appeared first on Stormpath User Identity API.


























