自动刷新用户的OAuth2令牌以访问Azure Graph API

zbdgwd5y  于 2023-10-15  发布在  其他
关注(0)|答案(1)|浏览(164)

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 &mdash; 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中的凭证。
它工作;应用程序可以读取电子邮件。但是,正如前面提到的,它确实要求每当服务器重新启动时(由于应用程序正在快速增强,每周会发生几次),都要重复手动重新验证过程。我们想要的是,当服务器重新启动时,它从数据库中获取令牌,而不需要人工干预。实现这一目标所需的最小变化是什么?

dhxwm5r4

dhxwm5r41#

  • 我有点坐立不安地等待对我的评论的回应,所以我只会在两种情况下解释它(输入受限设备和不)。

为了回答您的具体问题,一般来说,有两种方法可以延长访问时间。您可以延长访问令牌的生存期,也可以使用刷新令牌。在OAuth 2.0/OIDC中,刷新令牌旨在作为一种获得新访问令牌的方式,而无需重新提示用户进行身份验证,并提供更好的安全性(主要是因为超长寿命令牌如果泄漏,访问令牌的寿命较短,但是一种无需重新认证即可获得新令牌的方法)。
以下是MS关于使用刷新令牌的文档:

// Line breaks for legibility only

POST /{tenant}/oauth2/v2.0/token HTTP/1.1
Host: https://login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded

client_id=535fb089-9ff3-47b6-9bfb-4f1264799865
&scope=https%3A%2F%2Fgraph.microsoft.com%2Fmail.read
&refresh_token=OAAABAAAAiL9Kn2Z27UubvWFPbm0gLWQJVzCTE9UkP3pSx1aXxUjq...
&grant_type=refresh_token
&client_secret=sampleCredentia1s    // NOTE: Only required for web apps. 
This secret needs to be URL-Encoded

它应该只需要你可以存储的东西,而不需要任何用户交互。
首先是OAuth 2.0的入门。有4原始流量或赠款类型。您可以忽略其中两个(* 因为安全性 *):隐式和资源所有者密码凭据。这就给我们留下了授权码和客户端凭据。

*客户端凭据

  • 其中 * 没有用户 * 的流。
  • 一个应用程序或服务器,甚至是一个物联网设备正在代表自己进行身份验证。你的问题提到了对用户进行身份验证,所以我假设这里不是这种情况。
    *授权码
  • 其中有用户的流。
  • 此流程提供了一个更好的机会,隐式和资源所有者密码。

因此,既然您的案例看起来需要用户身份验证(和授权),那么Authorization Code就是要使用的流。
如果我们坚持原来的4个。这也是为什么OAuth/OIDC如此复杂的部分原因。哦,是的,OIDC是为*身份验证*而添加的,是一个位于OAuth 2.0之上的层。后来在原来的规格中也加入了一些东西。
现在,通常情况下,没有理由转移到任何在原始规范之后添加的更新内容 *,除非 * 原始流程不适合您的用例。或者,你可以看看best current practices RFC,但它确实有相当多的细节。

  • Some* examples -如果你的情况最适合隐式流,不应该再使用,描述它的用途,那么你可以看看组合Authz Code with PKCE。另一个例子是,在原生/移动的应用程序中发现了一些漏洞,这些漏洞也可以通过PKCE的Authz Code来缓解。还有一个例子,因为它与你的问题中的一些东西有关,如果你正在为一个输入受限的设备构建一个应用程序,(例如,你有没有感觉到在电视遥控器上输入电子邮件和密码到视频游戏控制器的痛苦?),那么Device Authorization Code flow就是专门为您创建的。

回到你的具体问题,但也不是对你的 * 具体 * 问题的回答(但是,冗长的OAuth 2.0入门的原因)。
在第一个代码块中,使用了DeviceCodeCredentialBuilderClientSecretCredentialBuilder。这些用于设备代码授权流程(也称为用于输入受限设备)和客户端凭证(也称为请求访问的应用程序是认证和授权访问的“* 用户 *”)。
如果您正在构建一个典型的Web应用程序,您可能希望使用AuthorizationCodeCredentialBuilder来使用授权代码流对用户进行身份验证。不过,看起来您正在使用客户端凭据流来获取服务器to access the MS Graph API的令牌?

相关问题