admin管理员组

文章数量:1418627

I have an app that connects to the Gmail API of its users. It authenticates by using the refreshToken to get an accessToken.

Libraries in use: Spring boot 3.4.1 -> Spring security 6.4.2

The refresh token is stored in the database and can be used for a long time, but is invalidated if password is changed for example. We need a way to get a new refresh token easily from an integration page, but cannot see how this can be done programatically with Spring security in a simple way.

Right now I have manually found the refreshToken by doing the following:

  1. Added .oauth2Login(Customizer.withDefaults()) to the SecurityFilterChain
  2. Logged in with the Google user, and set a breakpoint in
class: DefaultAuthorizationCodeTokenResponseClient
method: public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
  1. Copy the refreshToken from the variables in the debugger

In my properties file I have:

spring.security.oauth2.client.registration.google.client-id={....}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
spring.security.oauth2.client.registration.google.scope=/,.profile,.email
spring.security.oauth2.client.provider.google.authorization-uri=;access_type=offline

So the question is simply Is there any Spring security features that can simplify this process of getting the refreshToken?

Ideal flow:

  1. User clicks "integrate Gmail" link
  2. Taken to Oauth 2.0 login
  3. redirects back to app after login and in the backend refreshToken has been saved to DB
private fun refreshAccessToken(refreshToken: String): Credential {
        val credential = GoogleCredential.Builder().setTransport(httpTransport)
            .setJsonFactory(JSON_FACTORY)
            .setClientSecrets(CLIENT_ID, CLIENT_SECRET)
            .build()
        credential.refreshToken = refreshToken
        credential.refreshToken()
        return credential
    }

I have an app that connects to the Gmail API of its users. It authenticates by using the refreshToken to get an accessToken.

Libraries in use: Spring boot 3.4.1 -> Spring security 6.4.2

The refresh token is stored in the database and can be used for a long time, but is invalidated if password is changed for example. We need a way to get a new refresh token easily from an integration page, but cannot see how this can be done programatically with Spring security in a simple way.

Right now I have manually found the refreshToken by doing the following:

  1. Added .oauth2Login(Customizer.withDefaults()) to the SecurityFilterChain
  2. Logged in with the Google user, and set a breakpoint in
class: DefaultAuthorizationCodeTokenResponseClient
method: public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
  1. Copy the refreshToken from the variables in the debugger

In my properties file I have:

spring.security.oauth2.client.registration.google.client-id={....}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
spring.security.oauth2.client.registration.google.scope=https://mail.google/,https://www.googleapis/auth/userinfo.profile,https://www.googleapis/auth/userinfo.email
spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google/o/oauth2/v2/auth?prompt=consent&access_type=offline

So the question is simply Is there any Spring security features that can simplify this process of getting the refreshToken?

Ideal flow:

  1. User clicks "integrate Gmail" link
  2. Taken to Oauth 2.0 login
  3. redirects back to app after login and in the backend refreshToken has been saved to DB
private fun refreshAccessToken(refreshToken: String): Credential {
        val credential = GoogleCredential.Builder().setTransport(httpTransport)
            .setJsonFactory(JSON_FACTORY)
            .setClientSecrets(CLIENT_ID, CLIENT_SECRET)
            .build()
        credential.refreshToken = refreshToken
        credential.refreshToken()
        return credential
    }
Share Improve this question asked Jan 29 at 13:13 pleasebenicepleasebenice 651 silver badge4 bronze badges 1
  • Please note that DefaultAuthorizationCodeTokenResponseClient is deprecated since 6.4 – Roar S. Commented Jan 29 at 15:16
Add a comment  | 

2 Answers 2

Reset to default 0
  1. Set up in Google Cloud Console:

    • Go to the Google Cloud Console
    • Select an existing project
    • Add your authorized redirect URI in web client credential like (http://localhost:8080/auth/google/callback)
  2. When a user needs to authenticate:

    • Redirect them to the authUrl generated in the auth URL
private fun buildOAuthUrl(): String {
    val encodedRedirectUri = URLEncoder.encode(redirectUri, StandardCharsets.UTF_8.toString())
    val encodedScope = scope.split(" ")
        .map { URLEncoder.encode(it.trim(), StandardCharsets.UTF_8.toString()) }
        .joinToString(" ")
    return UriComponentsBuilder.fromUriString(oauthUrl)
        .queryParam("client_id", clientId)
        .queryParam("redirect_uri", encodedRedirectUri)
        .queryParam("response_type", "code")
        .queryParam("scope", encodedScope)
        .queryParam("access_type", "offline")
        .queryParam("prompt", "consent")
        .build()
        .toUriString()
}
  • They'll be prompted to grant access to their Gmail Google will redirect back to your callback URL with an auth code
  1. After getting auth code:

    • We are calling GoogleAuthorizationCodeTokenRequest() to generate refresh token
val response = GoogleAuthorizationCodeTokenRequest(
            httpTransport,
            jsonFactory,
            clientId,
            clientSecret,
            authCode(We will get from the callback URL),
            redirectUri
        ).setGrantType("authorization_code").execute()

    val refreshToken = response.refreshToken
  • We are getting refresh token from the response.refreshToken

  • After We are update our DB table with new generated refresh token with current user email

This is the full code that works below.

Note in the frontend the first communication is to /generate-token which builds the Oauth url where the user logs in.

Once the user has logged in there is a callback to /oauth2/callback/google with the authCode as paramter that is used to get the refreshToken.

Using @dip-m's answer.


@Controller
class GmailController(
    private val securityUtils: SecurityUtils,
    private val googleOAuthService: GoogleOAuthService
) {
    @GetMapping("/settings")
    fun getSettingsPage(model: Model): String {
        if (!model.containsAttribute("tokenGrantSuccess")) {
            model.addAttribute("tokenGrantSuccess", model.getAttribute("tokenGrantSuccess"))
        } else {
            model.addAttribute("tokenGrantFail", false)
        }
        return "setting"
    }

    @GetMapping("/oauth2/callback/google")
    fun refreshTokenCallBack(
        redirectAttributes: RedirectAttributes,
        @RequestParam("code") authCode: String,
    ): String {
        val currentUserEmail = securityUtils.getCurrentUser()?.email
            ?: throw OAuthException("User not authenticated")

        googleOAuthService.handleOAuthCallback(authCode, currentUserEmail)
        redirectAttributes.addFlashAttribute("tokenGrantSuccess", true)
        return "redirect:/settings"
    }
}

@Service
class GoogleOAuthService(
    private val jdbcClient: JdbcClient,
    @Value("\${spring.security.oauth2.client.registration.google.client-id}") private val clientId: String,
    @Value("\${spring.security.oauth2.client.registration.google.client-secret}") private val clientSecret: String,
    @Value("\${spring.security.oauth2.client.registration.google.redirect-uri}") private val redirectUri: String,
    @Value("\${spring.security.oauth2.oauthUrl}") private val oauthUrl: String,
    @Value("\${spring.security.oauth2.client.registration.google.scope}") private val scope: String
) {
    private val logger = LoggerFactory.getLogger(GoogleOAuthService::class.java)
    private val jsonFactory: JsonFactory = GsonFactory.getDefaultInstance()
    private val httpTransport: NetHttpTransport = GoogleNetHttpTransport.newTrustedTransport()

    fun handleOAuthCallback(authCode: String, userEmail: String) {
        try {
            val response = GoogleAuthorizationCodeTokenRequest(
                httpTransport,
                jsonFactory,
                clientId,
                clientSecret,
                authCode,
                redirectUri
            ).setGrantType("authorization_code").execute()

            updateUserRefreshToken(userEmail, response.refreshToken)
        } catch (e: Exception) {
            logger.error("Failed to handle OAuth callback", e)
            throw OAuthException("Failed to process OAuth callback: ${e.message}")
        }
    }

    private fun updateUserRefreshToken(email: String, refreshToken: String) {
        jdbcClient.sql("""
            UPDATE users SET refresh_token = :refreshToken WHERE email = :email
        """.trimIndent())
            .param("refreshToken", refreshToken)
            .param("email", email)
            .update()
        logger.info("User refresh token successfully updated for email $email")
    }

    fun generateOAuthUrl(): String {
        validateOAuthConfig()
        return try {
            buildOAuthUrl()
        } catch (e: Exception) {
            logger.error("Failed to generate OAuth URL", e)
            throw OAuthException("Failed to generate OAuth URL: ${e.message}")
        }
    }

    private fun buildOAuthUrl(): String {
        val encodedRedirectUri = URLEncoder.encode(redirectUri, StandardCharsets.UTF_8.toString())
        val encodedScope = URLEncoder.encode(scope.replace(",", " "), StandardCharsets.UTF_8.toString())

        return UriComponentsBuilder.fromUriString(oauthUrl)
            .queryParam("client_id", clientId)
            .queryParam("redirect_uri", encodedRedirectUri)
            .queryParam("response_type", "code")
            .queryParam("scope", encodedScope)
            .queryParam("access_type", "offline")
            .queryParam("prompt", "consent")
            .build()
            .toUriString()
    }

    private fun validateOAuthConfig() {
        val missingFields = buildList {
            if (clientId.isBlank()) add("clientId")
            if (redirectUri.isBlank()) add("redirectUri")
            if (oauthUrl.isBlank()) add("oauthUrl")
            if (scope.isBlank()) add("scope")
        }

        if (missingFields.isNotEmpty()) {
            val errorMessage = "Missing required fields: ${missingFields.joinToString(", ")}"
            logger.error(errorMessage)
            throw OAuthException(errorMessage)
        }
    }
}

class OAuthException(message: String) : RuntimeException(message)

@RestController
class GmailRestController(private val googleOAuthService: GoogleOAuthService) {
    @GetMapping("/generate-token")
    fun generateRefreshToken(): String = googleOAuthService.generateOAuthUrl()
}

Frontend code, ScriptsStylingAndMeta just imports HTMX, Shoelace and Bootstrap:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf." xmlns:hx-on="http://www.w3./1999/xhtml">
<head>
  <div th:replace="~{fragments :: ScriptsStylingAndMeta}"/>
  <title>Settings Page</title>
  <style>
    .settings-card {
      background-color: var(--sl-color-neutral-0);
      border-radius: var(--sl-border-radius-medium);
      box-shadow: var(--sl-shadow-medium);
      padding: var(--sl-spacing-large);
    }

    .settings-description {
      color: var(--sl-color-neutral-600);
      margin-bottom: var(--sl-spacing-medium);
    }

    .success-message {
      display: flex;
      align-items: center;
      gap: var(--sl-spacing-small);
    }

    .hidden {
      display: none;
    }
  </style>
</head>
<body>
<div th:replace="~{fragments :: header}"/>

<div class="container py-5">
  <sl-card class="settings-card">
    <div class="settings-section">
      <sl-header class="mb-4">
        <h2>Google Integration</h2>
      </sl-header>

      <div id="integration-status" th:fragment="status" class="stack">
        <div th:if="${tokenGrantSuccess}" class="success-message" role="status">
          <sl-alert variant="success" open>
            <sl-icon slot="icon" name="check2-circle"></sl-icon>
            Refresh token has been successfully configured!
          </sl-alert>
        </div>

        <div th:unless="${tokenGrantSuccess}">
          <p class="settings-description">
            Generate a refresh token to enable Gmail integration with your account.
          </p>
          <sl-button
                  variant="primary"
                  size="large"
                  hx-get="/generate-token"
                  hx-trigger="click"
                  hx-swap="none"
                  hx-indicator="this"
                  hx-on::after-request="handleTokenResponse(event)"
                  aria-label="Generate Gmail refresh token"
          >
            <sl-icon slot="prefix" name="key"></sl-icon>
            Generate Refresh Token
          </sl-button>

          <sl-alert
                  variant="danger"
                  class="error-message hidden"
                  id="error-message"
          >
            <sl-icon slot="icon" name="exclamation-triangle"></sl-icon>
            Failed to generate token. Please try again.
          </sl-alert>
        </div>
      </div>
    </div>
  </sl-card>
</div>

<script>
  document.addEventListener('DOMContentLoaded', () => {
    // Ensure Shoelace components are defined
    customElements.whenDefined('sl-alert').then(() => {
      const errorAlert = document.getElementById('error-message');

      function handleTokenResponse(event) {
        const response = event.detail.xhr.response;
        if (response) {
          window.location.href = response;
        } else {
          console.error('Invalid response received');
          errorAlert.classList.remove('hidden');
          setTimeout(() => {
            errorAlert.classList.add('hidden');
          }, 5000);
        }
      }

      window.handleTokenResponse = handleTokenResponse;
    });
  });
</script>
</body>
</html>

本文标签: Getting refresh token for gmail API with Spring SecurityStack Overflow