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:
- Added
.oauth2Login(Customizer.withDefaults())
to the SecurityFilterChain - Logged in with the Google user, and set a breakpoint in
class: DefaultAuthorizationCodeTokenResponseClient
method: public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
- 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:
- User clicks "integrate Gmail" link
- Taken to Oauth 2.0 login
- 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:
- Added
.oauth2Login(Customizer.withDefaults())
to the SecurityFilterChain - Logged in with the Google user, and set a breakpoint in
class: DefaultAuthorizationCodeTokenResponseClient
method: public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
- 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:
- User clicks "integrate Gmail" link
- Taken to Oauth 2.0 login
- 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
|
2 Answers
Reset to default 0Set 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)
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
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
版权声明:本文标题:Getting refresh token for gmail API with Spring Security - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1745295970a2652079.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
DefaultAuthorizationCodeTokenResponseClient
is deprecated since 6.4 – Roar S. Commented Jan 29 at 15:16