Spring Boot security configuration
It adds the configuration for both basic and form login authent from the front end.
An interesting thing is that we need to override some handlers to prevent Spring from redirecting to a front web page. That relies us from also configuring the front end with CORS policy.
@Override protected void configure(HttpSecurity http) throws Exception { http.cors() .and() // hard time with Angular : So disable for now .csrf() .disable() // static resources (not sensitive here) and actuator : permit all .authorizeRequests() .antMatchers("/*.js", "/favicon.ico", "/actuator", "/actuator/*") .permitAll() .and() //permit all to register as a user .authorizeRequests() .antMatchers(HttpMethod.POST, "/api/users") .permitAll() // all other requests is allowed only for authenticated user .anyRequest() .authenticated() .and() //enable basic authent (sent for each request) .httpBasic() .and() // enable form login authent (authent request send a single time by post method and then session id cookie returned by server) // override successHandler to prevent redirection backend to frontend (CORS issue) .formLogin() .loginProcessingUrl("/processLogin") .successHandler(successHandler()) .loginPage("http://localhost:4200/myLogin") .and() // logout authorized and override logoutSuccessHandler to prevent redirection backend to frontend (CORS issue) .logout() .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET")) .logoutSuccessHandler(logoutSuccessfulHandler()) .permitAll(); } private LogoutSuccessHandler logoutSuccessfulHandler() { return new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setContentType("text/html;charset=UTF-8"); response.getWriter().println("logout done"); } }; } private AuthenticationSuccessHandler successHandler() { return new SimpleUrlAuthenticationSuccessHandler() { public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setContentType("text/html;charset=UTF-8"); response.getWriter().println("authenticated"); } }; } |
Angular : Basic authentication
Here is a request that sends in the header the authentication.
The target url is a processlogin because in some case we can use basic authorization with a login page if the user has to be given by the application user.
import { User } from "../user"; import { Observable } from "rxjs/Observable"; import { AuthUser } from "../AuthUser"; import { IAuthentService } from "./IAuthentService"; import { HttpHeaders, HttpClient, HttpResponse } from "@angular/common/http"; import { Injectable } from "@angular/core"; @Injectable() export class BasicAuthentService implements IAuthentService { constructor(private http: HttpClient) { } connect(user: User): Observable<AuthUser> { const userpass = btoa(user.username + ':' + user.password) const headers = new HttpHeaders({ 'Content-type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic ' + userpass }); return this.http.get<AuthUser>('http://localhost:8282/api/users/processLoginXXX', { headers, observe: 'response' }) .map<HttpResponse<any>, AuthUser>( response => { const respBody = response.body console.info("RESP BODY=" + JSON.stringify(respBody)) console.info("RESP HEADERS=" + JSON.stringify(response.headers)) const authUser: AuthUser = JSON.parse(JSON.stringify(respBody)) if (authUser.authenticated) { localStorage.setItem('basic_pass', userpass); return authUser } throw new Error("how are you there ? You are not an authenticated user after login") }) .catch(this.processError) } processError(err): Observable<any> { console.log("error=" + JSON.stringify(err)) return Observable.throw(err.message) } } |
And here is the interceptor associated that add the basic header in each request if the user is authenticated, otherwise redirect to the login page:
import { HttpHandler, HttpInterceptor, HttpRequest, HttpResponse, HttpErrorResponse, HttpEvent, HttpHeaders } from "@angular/common/http"; import { Observable } from "rxjs/Observable"; import { UserSessionService } from "../user-session.service"; import { Router } from "@angular/router"; export class BasicAuthentInterceptor implements HttpInterceptor { constructor(public userSessionService: UserSessionService, private router: Router) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { if (req.url.endsWith("/api/users/processLoginXXX")) { return next.handle(req); } const userAuth = this.userSessionService.getUser() if (!userAuth) { this.router.navigateByUrl('/myLogin') return } else { const pass = localStorage.getItem('basic_pass'); const headers = new HttpHeaders({ 'Authorization': 'Basic ' + pass }); req = req.clone({ headers }) } return next.handle(req); } } |
Angular : Form Login authentication
Here is a request that sends an authentication request to Spring Boot.
An important thing is the withCredentials property specified in the request.
That is relevant for cross origin requests : it specifies that the client app needs that access control header of the response allows credentials.
It is the single way to allow browsers of a domain to use credentials transmitted on another domain during a cross domain request.
import { User } from "../user"; import { Observable } from "rxjs/Observable"; import { AuthUser } from "../AuthUser"; import { IAuthentService } from "./IAuthentService"; import { HttpHeaders, HttpClient, HttpResponse } from "@angular/common/http"; import { Inject, Injectable } from "@angular/core"; @Injectable() export class FormAuthentService implements IAuthentService { constructor(private http: HttpClient) { } connect(user: User): Observable<AuthUser> { const headers = new HttpHeaders({ 'Content-type': 'application/x-www-form-urlencoded', }); const body = `username=${user.username}&password=${user.password}` return this.http.post('http://localhost:8282/processLogin', body, { headers, observe: 'response', responseType: 'text', withCredentials: true }) .map<HttpResponse<string>, AuthUser>( response => { const authUser: AuthUser = new AuthUser(true, user.username) return authUser }) .catch(this.processError) } processError(err): Observable<any> { console.log("error=" + JSON.stringify(err)) return Observable.throw(err.message) } } |
Here is the interceptor associated to that authentication strategy.
It adds withCredentials: true
in all requests before sending that and it has a hack to intercept a need of login but that may be improved (to do). I
import { HttpHandler, HttpInterceptor, HttpRequest, HttpResponse, HttpErrorResponse, HttpEvent, HttpHeaders } from "@angular/common/http"; import { Observable } from "rxjs/Observable"; import { UserSessionService } from "../user-session.service"; import { Router } from "@angular/router"; export class FormAuthentInterceptor implements HttpInterceptor { constructor(public userSessionService: UserSessionService, private router: Router) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const userAuth = this.userSessionService.getUser() if (userAuth) { req = req.clone({ withCredentials: true }) } return next.handle(req).do((event: HttpEvent<any>) => { // Nothing in the successful case }, (err: any) => { if (err instanceof HttpErrorResponse) { /* Hack : When an xmlhttprequest from the front fails because the user is not authenticated, the backend (spring boot) redirects to 'front':4200/myLogin but the xmlhttprequest response cannot change the current location of the browser. So we need to do things in two steps : - intercepts here the 404 produced by the 4200/myLogin request and navigate via the router to 4200/myLogin */ if (err.status === 404 && "http://localhost:4200/myLogin" === err.url) { console.info("redirect to /myLogin") this.router.navigateByUrl('/myLogin') } if (err.status === 401) { // currently not needed } } }); } } |