OAuth2 With Google, Spring Boot And Angular

I choose Google to log my users. Because I don’t want to handle the users’ passwords and everybody has a Google account.

That’s why I need to connect my Spring Boot application to the Google sign-in.

Google uses the OAuth2 authentication protocol to sign-in the users. However there are some considerations to take into account when configuring Spring Security.

In this article, I describe how to create the Google OAuth2 client, how to connect Spring Security to Google OAuth2 workflow, and have an Angular frontend to communicate with my Spring Boot application.

You can find the full project at Github.

And for a more detailed explanation, check the following video.

OAuth2 Workflow

Let me first explain the workflow I will use to authenticate a user in my application.

OAuth2 workflow with Google

Once the user loads the frontend, React asks the backend for a Google URL where the user can sign in.

The user needs to click on this URL to open the Google sign-in form.

Once the user is logged, Google knows where to redirect the user: to the frontend with a unique code.

The frontend has no information about the authentication workflow. Its only role is to redirect the generated code from Google to the backend.

With this redirection, the backend obtains a JWT generated by Google. This JWT is then used by the frontend to ensure each request is authenticated by a Google user.

Create OAuth2 Client

To create the OAuth2 Google Client, I need to create a GCP project. Once created the project, I go APIs & Services and OAuth consent screen.

Google OAuth2 consent screen

This section is to configure the page the user sees when it connects to my application. It’s the page which says “You’re about to connect to TheDevWorldApi, do you trust it?”.

This page has no configuration but the name, contact emails and the logo. But on the second screen, I must configure the scopes, I must add the email, profile and openid scopes. This tells Google to share the profile of the user once authenticated.

Google OAuth2 consent screen scopes

Once I have my consent screen, I need to create the OAuth2 Client. This defines the way an application can use Google’s authentication.

To create a new client, I go to APIs & Services and Credentials. I hit Create credentials and I chose Web application.

Google OAuth2 Web Client Creation

After choosing a technical name for my client, I must add the URLs which will try to connect. In my case, I only add the Angular URL of my local machine.

Finally, I must also add the redirect URL, the URL Google calls once the authentication is done. Here, I put the homepage of my Angular application.

That’s all on the Google side, let’s now create the Angular application.

Create Frontend

My frontend application needs the following components:

  • An HTTP service, which can distinguish from public endpoints to protected endpoints, as the protected endpoints need an Authorization header;
  • The homepage allows to go to the login form, or display protected content. As the homepage is where Google will redirect the user after the authentication, I must verify the received code from Google;
  • And finally, the Login form, where I display a button which redirects to the Google sign-in form.

Let’s start by creating the custom HTTP service to distinguish the authenticated requests, the public requests and the login request.

@Injectable({
  providedIn: 'root'
})
export class MyHttpService {

  token: string = "";

  constructor(private http: HttpClient) { }

  get(url: string): any {
    return this.http.get("http://localhost:8080" + url);
  }

  getPrivate(url: string): any {
    return this.http.get("http://localhost:8080" + url, {headers: new HttpHeaders({"Authorization": "Bearer " + this.token})});
  }

  getToken(code: string): Observable<boolean> {
    return this.http.get<Token>("http://localhost:8080/auth/callback?code=" + code, {observe: "response"})
      .pipe(map((response: HttpResponse<Token>) => {
        if (response.status === 200 && response.body !== null) {
          this.token = response.body.token;
          return true;
        } else {
          return false;
        }
      }));
  }
}

I’ve created 3 methods:

  • get: for the public endpoints;
  • getPrivate: for the protected endpoints, where I add the JWT in the Authorization endpoint;
  • and getToken: to obtain the JWT from the Google code;

Let’s continue with the Login component, where I display the Google URL to sign in.

@Component({
  selector: 'app-login-form',
  templateUrl: './login-form.component.html',
  styleUrls: ['./login-form.component.css']
})
export class LoginFormComponent {

  url: string = "";

  constructor(private http: MyHttpService) {}

  ngOnInit(): void {
     this.http.get("/auth/url").subscribe((data: any) => this.url = data.authURL);
  }

}

With the HTML.

<div>

  <h2>Login form</h2>

  <a href="{{url}}" >Sign in with Google</a>

</div>

It’s a simple component, where I only display a link with the Google URL. When loading the component, I call the backend to create the Google’s URL.

And finally, the App component which fetches the JWT or displays the Login component.

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  componentToShow: string = "welcome";

  constructor(private http: MyHttpService, private route: ActivatedRoute) {}

  ngOnInit(): void {
    this.route.queryParams
      .subscribe(params => {
        if (params["code"] !== undefined) {
          this.http.getToken(params["code"]).subscribe(result => {
            if (result === true) {
              this.componentToShow = "protected";
            } else {
              this.componentToShow = "welcome";
            }
          });
        }
      }
    );
  }

}

Create Backend

The Angular frontend is ready. Let’s now create a Spring Boot application with Spring Security. I must also add the dependency Spring Security OAuth2 Resources Server, as my backend takes the role of a Resources Server in the OAuth2 workflow.

Let’s go directly to the Spring Security configuration.

public class SecurityConfig {

    private final WebClient userInfoClient;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .cors(Customizer.withDefaults())
                .exceptionHandling(customizer -> customizer.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)))
                .sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests((authorize) -> authorize
                .requestMatchers("/", "/auth/**", "/public/**").permitAll()
                .anyRequest().authenticated()
        )
                .oauth2ResourceServer(c -> c.opaqueToken(Customizer.withDefaults()));
        ;
        return http.build();
    }

    @Bean
    public OpaqueTokenIntrospector introspector() {
        return new GoogleOpaqueTokenIntrospector(userInfoClient);
   

I have the regular configuration for the CORS and ExceptionHandling. I must configure my application as Stateless, and indicate which are the public path and which are the protected path. And at the end, I say that it’s a Resources Server with an Opaque Token.

If I want to use the Google authentication, I must configure an Opaque Token introspector. Why? Because I think that Google has blocked some regular endpoints of the OAuth2 workflow. So, the Spring Security default configuration is unable to obtain the user’s information from Google.

That said, let’s see what the Opaque Token Instrospector looks like.

public class GoogleOpaqueTokenIntrospector implements OpaqueTokenIntrospector {

    private final WebClient userInfoClient;

    @Override
    public OAuth2AuthenticatedPrincipal introspect(String token) {
        UserInfo userInfo = userInfoClient.get()
                .uri( uriBuilder -> uriBuilder
                        .path("/oauth2/v3/userinfo")
                        .queryParam("access_token", token)
                        .build())
                .retrieve()
                .bodyToMono(UserInfo.class)
                .block();
        Map<String, Object> attributes = new HashMap<>();
        attributes.put("sub", userInfo.sub());
        attributes.put("name", userInfo.name());
        return new OAuth2IntrospectionAuthenticatedPrincipal(userInfo.name(), attributes, null);
    }
}

It’s a bean where I request an endpoint of Google to obtain the user’s name.

The Web Client Configuration builds the WebClient.

public class WebClientConfig {

    @Value("${spring.security.oauth2.resourceserver.opaque-token.introspection-uri}")
    private String introspectUri;

    @Bean
    public WebClient userInfoClient() {
        return WebClient.builder().baseUrl(introspectUri).build();
    }
}

And the application YAML file contains the URL and the Google’s Client Id and Client Secret I’ve obtained from GCP.

spring:
  security:
    oauth2:
      resourceserver:
        opaque-token:
          introspection-uri: https://www.googleapis.com/
          clientId: "..."
          clientSecret: "..."

And finally, the Authentication endpoints where I build the Google’s URL for Sign-In, and handle the code of the callback.

public class AuthController {

    @Value("${spring.security.oauth2.resourceserver.opaque-token.clientId}")
    private String clientId;

    @Value("${spring.security.oauth2.resourceserver.opaque-token.clientSecret}")
    private String clientSecret;


    @GetMapping("/auth/url")
    public ResponseEntity<UrlDto> auth() {
        String url = new GoogleAuthorizationCodeRequestUrl(clientId,
                "http://localhost:4200",
                Arrays.asList(
                        "email",
                        "profile",
                        "openid")).build();

        return ResponseEntity.ok(new UrlDto(url));
    }

    @GetMapping("/auth/callback")
    public ResponseEntity<TokenDto> callback(@RequestParam("code") String code) throws URISyntaxException {

        String token;
        try {
            token = new GoogleAuthorizationCodeTokenRequest(
                    new NetHttpTransport(), new GsonFactory(),
                    clientId,
                    clientSecret,
                    code,
                    "http://localhost:4200"
            ).execute().getAccessToken();
        } catch (IOException e) {
            System.err.println(e.getMessage());
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }

        return ResponseEntity.ok(new TokenDto(token));
    }
}

After the user is authenticated, I just need to use the AuthenticationPrincipal annotation to read the information of the user.

@RestController
public class PrivateController {

    @GetMapping("/messages")
    public ResponseEntity<MessageDto> privateMessages(@AuthenticationPrincipal(expression = "name") String name) {
        return ResponseEntity.ok(new MessageDto("private content " + name));
    }
}

Conclusion

Usually, Spring Security has all pre-configured to call the OAuth2 endpoints of any client. But Google has some additional security restrictions which makes me need the Opaque Token Introspector.

As said at the beginning, you can find the full project at Github, and a more detailed explanation in the following video.


Never Miss Another Tech Innovation

Concrete insights and actionable resources delivered straight to your inbox to boost your developer career.

My New ebook, Best Practices To Create A Backend With Spring Boot 3, is available now.

Best practices to create a backend with Spring Boot 3

Leave a comment

Discover more from The Dev World - Sergio Lema

Subscribe now to keep reading and get access to the full archive.

Continue reading