TL;DR:我很乐意承认我没有完全理解OAuth2流,也不知道如何持久化对用户电子邮件收件箱的访问。
为了扩展前面的句子,我们有一个KotlinSping Boot 应用程序,其中一个功能是读取和处理发送到[[email protected]](https://stackoverflow.com/cdn-cgi/l/email-protection)
的电子邮件。由于MS现在通过 * OAuth2强制认证,因此我们必须实现客户端逻辑。我已经做到了这一点,但是每当服务器重新启动时,我必须通过浏览器页面重新执行手动身份验证流程。我理解,使用OAuth2有必要这样做一次,但只有一次,之后应该可以在没有人为干预的情况下以编程方式刷新令牌;特别是在应用程序重新启动的情况下。显然,这意味着访问和刷新令牌必须持久化(存储在数据库表中)。
首先,我从MS的文档中复制了一些示例代码并转换为Kotlin:
package au.com.ourcompany.email
import com.azure.core.credential.TokenRequestContext
import com.azure.identity.ClientSecretCredentialBuilder
import com.azure.identity.DeviceCodeCredentialBuilder
import com.azure.identity.DeviceCodeInfo
import com.microsoft.graph.authentication.TokenCredentialAuthProvider
import com.microsoft.graph.requests.GraphServiceClient
import java.time.Instant
import java.util.function.Consumer
class MSOauth2Graph(clientId: String,
tenantId: String,
clientSecret: String,
graphUserScopes: List<String>,
challenge: Consumer<DeviceCodeInfo?>)
{
private val deviceCodeCredential = DeviceCodeCredentialBuilder()
.clientId(clientId)
.tenantId(tenantId)
.challengeConsumer(challenge)
.build()
private val clientSecretCredential = ClientSecretCredentialBuilder()
.clientId(clientId)
.tenantId(tenantId)
.clientSecret(clientSecret)
.build()
private val myself = GraphServiceClient.builder()
.authenticationProvider(TokenCredentialAuthProvider(graphUserScopes, deviceCodeCredential))
.buildClient()
.me()
private val graphClient = GraphServiceClient.builder()
.authenticationProvider(TokenCredentialAuthProvider(authSources, clientSecretCredential))
.buildClient()
private val requestContext = TokenRequestContext().apply { addScopes(*graphUserScopes.toTypedArray()) }
fun getUserToken(): String = deviceCodeCredential.getToken(requestContext).block()!!.token
@Throws(java.lang.Exception::class)
fun getAppOnlyToken(): String = clientSecretCredential.getToken(requestContext).block()!!.token
fun getUser() = myself
.buildRequest()
.select("displayName,mail,userPrincipalName")
.get()
fun getMailboxes() = myself
.mailFolders("inbox")
.childFolders()
.buildRequest()
.get()
// Ensure client isn't null@get:Throws(Exception::class)val inbox:MessageCollectionPage
fun getInbox(pageSize: Int, pageCount: Int, folderName: String, afterStamp: Instant) = myself
.mailFolders(folderName)
.messages()
.buildRequest()
.select("from,receivedDateTime,subject,body")
.filter("receivedDateTime ge $afterStamp")
.skip(pageSize * pageCount)
.top(pageSize)
.orderBy("receivedDateTime DESC")
.get()
}
private val authSources = listOf("https://graph.microsoft.com/.default")
然后使用它来获取支持用户的凭据:
private val logger = LoggerFactory.getLogger(this.javaClass)
private var userCode: String? = null
private var authenticationURL: String? = null
/**
* AcceptChallenge — called when challenge is accepted
*/
private inner class AcceptChallenge: Consumer<DeviceCodeInfo?>
{
override fun accept(challenge: DeviceCodeInfo?) {
if (challenge != null) {
userCode = challenge.userCode
authenticationURL = challenge.verificationUrl
logger.info("CHALLENGE: U=\"${challenge.userCode}\" V=\"${challenge.verificationUrl}\" D=\"${challenge.deviceCode}\" X=\"${challenge.expiresOn}\"")
} else
userCode = null
}
}
private val graph = MSOauth2Graph(platformConfiguration.credentials.azureOAuth2AppId,
platformConfiguration.credentials.azureOAuth2TenantId,
platformConfiguration.credentials.azureSecretValue,
platformConfiguration.deployment.azureGraphUserScopes.split(",").dropLastWhile{it.isEmpty()},
AcceptChallenge())
@OptIn(DelicateCoroutinesApi::class)
@ResponseBody @GetMapping("/azlogin")
private fun loginToAD(): ResponseEntity<String> {
GlobalScope.launch {
graph.getUser().let { user ->
logger.info("logged-in to AD: ${user?.id}")
}
}
// This is crap but using semaphores etc. doesn't work
while (authenticationURL == null)
Thread.sleep(1000)
return ResponseEntity.ok("""
$standardPagePreamble
<title>Authenticate support user to Azure AD</title></head>
<body>
<form class="onerow" action="/"><input class="onerow" type="submit" value="Home"/></form>
<br/><br/>
Click <a href="$authenticationURL" target="_blank">here</a> and enter the code $userCode
</body></html>
""".trimIndent())
}
该地段提供了一个路由(/azlogin
),人类可以“访问”并获得userCode
中的凭证。
它工作;应用程序可以读取电子邮件。但是,正如前面提到的,它确实要求每当服务器重新启动时(由于应用程序正在快速增强,每周会发生几次),都要重复手动重新验证过程。我们想要的是,当服务器重新启动时,它从数据库中获取令牌,而不需要人工干预。实现这一目标所需的最小变化是什么?
1条答案
按热度按时间dhxwm5r41#
为了回答您的具体问题,一般来说,有两种方法可以延长访问时间。您可以延长访问令牌的生存期,也可以使用刷新令牌。在OAuth 2.0/OIDC中,刷新令牌旨在作为一种获得新访问令牌的方式,而无需重新提示用户进行身份验证,并提供更好的安全性(主要是因为超长寿命令牌如果泄漏,访问令牌的寿命较短,但是一种无需重新认证即可获得新令牌的方法)。
以下是MS关于使用刷新令牌的文档:
它应该只需要你可以存储的东西,而不需要任何用户交互。
首先是OAuth 2.0的入门。有4原始流量或赠款类型。您可以忽略其中两个(* 因为安全性 *):隐式和资源所有者密码凭据。这就给我们留下了授权码和客户端凭据。
*客户端凭据
*授权码
因此,既然您的案例看起来需要用户身份验证(和授权),那么Authorization Code就是要使用的流。
如果我们坚持原来的4个。这也是为什么OAuth/OIDC如此复杂的部分原因。哦,是的,OIDC是为*身份验证*而添加的,是一个位于OAuth 2.0之上的层。后来在原来的规格中也加入了一些东西。
现在,通常情况下,没有理由转移到任何在原始规范之后添加的更新内容 *,除非 * 原始流程不适合您的用例。或者,你可以看看best current practices RFC,但它确实有相当多的细节。
回到你的具体问题,但也不是对你的 * 具体 * 问题的回答(但是,冗长的OAuth 2.0入门的原因)。
在第一个代码块中,使用了
DeviceCodeCredentialBuilder
和ClientSecretCredentialBuilder
。这些用于设备代码授权流程(也称为用于输入受限设备)和客户端凭证(也称为请求访问的应用程序是认证和授权访问的“* 用户 *”)。如果您正在构建一个典型的Web应用程序,您可能希望使用
AuthorizationCodeCredentialBuilder
来使用授权代码流对用户进行身份验证。不过,看起来您正在使用客户端凭据流来获取服务器to access the MS Graph API的令牌?