Skip to content

Commit

Permalink
feat: 支持PKCE模式OAuth认证 TencentBlueKing#1124 (TencentBlueKing#1142)
Browse files Browse the repository at this point in the history
* feat: 支持PKCE模式OAuth认证 TencentBlueKing#1124

* feat: 支持PKCE模式OAuth认证 TencentBlueKing#1124

* feat: 支持PKCE模式OAuth认证 TencentBlueKing#1124

* feat: 支持PKCE模式OAuth认证 TencentBlueKing#1124

* feat: 支持PKCE模式OAuth认证 TencentBlueKing#1124

* feat: 支持PKCE模式OAuth认证 TencentBlueKing#1124
  • Loading branch information
yaoxuwan authored Sep 11, 2023
1 parent 76dbddc commit ef37e05
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@

package com.tencent.bkrepo.auth.pojo.oauth

data class CreateTokenRequest(
data class AuthorizeRequest(
val clientId: String,
val clientSecret: String,
val code: String
val state: String,
val scope: String?,
val nonce: String?,
val codeChallenge: String?,
val codeChallengeMethod: String?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@ data class GenerateTokenRequest(
val clientId: String?,
val clientSecret: String?,
val refreshToken: String?,
val scope: String?
val scope: String?,
val codeVerifier: String?
)
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
package com.tencent.bkrepo.auth.controller.user

import com.tencent.bkrepo.auth.constant.AUTH_API_OAUTH_PREFIX
import com.tencent.bkrepo.auth.pojo.oauth.AuthorizeRequest
import com.tencent.bkrepo.auth.pojo.oauth.AuthorizedResult
import com.tencent.bkrepo.auth.pojo.oauth.GenerateTokenRequest
import com.tencent.bkrepo.auth.pojo.oauth.JsonWebKeySet
Expand Down Expand Up @@ -59,9 +60,19 @@ class OauthAuthorizationController @Autowired constructor(
@RequestParam("client_id") clientId: String,
state: String,
scope: String?,
nonce: String?
nonce: String?,
@RequestParam("code_challenge") codeChallenge: String?,
@RequestParam("code_challenge_method") codeChallengeMethod: String?
): Response<AuthorizedResult> {
val authorizedResult = oauthAuthorizationService.authorized(clientId, state, scope, nonce)
val request = AuthorizeRequest(
clientId = clientId,
state = state,
scope = scope,
nonce = nonce,
codeChallenge = codeChallenge,
codeChallengeMethod = codeChallengeMethod
)
val authorizedResult = oauthAuthorizationService.authorized(request)
return ResponseBuilder.success(authorizedResult)
}

Expand All @@ -79,9 +90,10 @@ class OauthAuthorizationController @Autowired constructor(
@RequestParam("client_id") clientId: String?,
@RequestParam("client_secret") clientSecret: String?,
@RequestParam("refresh_token") refreshToken: String?,
scope: String?
scope: String?,
@RequestParam("code_verifier") codeVerifier: String?,
) {
val request = GenerateTokenRequest(code, grantType, clientId, clientSecret, refreshToken, scope)
val request = GenerateTokenRequest(code, grantType, clientId, clientSecret, refreshToken, scope, codeVerifier)
if (request.grantType == "refresh_token") {
oauthAuthorizationService.refreshToken(request)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

package com.tencent.bkrepo.auth.service

import com.tencent.bkrepo.auth.pojo.oauth.AuthorizeRequest
import com.tencent.bkrepo.auth.pojo.oauth.AuthorizedResult
import com.tencent.bkrepo.auth.pojo.oauth.GenerateTokenRequest
import com.tencent.bkrepo.auth.pojo.oauth.JsonWebKeySet
Expand All @@ -42,7 +43,7 @@ interface OauthAuthorizationService {
/**
* 确认授权
*/
fun authorized(clientId: String, state: String, scope: String?, nonce: String?): AuthorizedResult
fun authorized(authorizeRequest: AuthorizeRequest): AuthorizedResult

/**
* 创建token
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import com.tencent.bkrepo.auth.message.AuthMessageCode
import com.tencent.bkrepo.auth.model.TAccount
import com.tencent.bkrepo.auth.model.TOauthToken
import com.tencent.bkrepo.auth.pojo.oauth.AuthorizationGrantType
import com.tencent.bkrepo.auth.pojo.oauth.AuthorizeRequest
import com.tencent.bkrepo.auth.pojo.oauth.AuthorizedResult
import com.tencent.bkrepo.auth.pojo.oauth.GenerateTokenRequest
import com.tencent.bkrepo.auth.pojo.oauth.IdToken
Expand All @@ -57,6 +58,7 @@ import com.tencent.bkrepo.common.api.util.JsonUtils
import com.tencent.bkrepo.common.api.util.Preconditions
import com.tencent.bkrepo.common.api.util.toJsonString
import com.tencent.bkrepo.common.api.util.toXmlString
import com.tencent.bkrepo.common.artifact.hash.HashAlgorithm
import com.tencent.bkrepo.common.redis.RedisOperation
import com.tencent.bkrepo.common.security.crypto.CryptoProperties
import com.tencent.bkrepo.common.security.util.JwtUtils
Expand All @@ -82,29 +84,36 @@ class OauthAuthorizationServiceImpl(
private val oauthProperties: OauthProperties
) : OauthAuthorizationService {

override fun authorized(clientId: String, state: String, scope: String?, nonce: String?): AuthorizedResult {
val userId = SecurityUtils.getUserId()
val client = accountRepository.findById(clientId)
.orElseThrow { ErrorCodeException(AuthMessageCode.AUTH_CLIENT_NOT_EXIST) }
val code = OauthUtils.generateCode()
override fun authorized(authorizeRequest: AuthorizeRequest): AuthorizedResult {
with(authorizeRequest) {
val userId = SecurityUtils.getUserId()
val client = accountRepository.findById(clientId)
.orElseThrow { ErrorCodeException(AuthMessageCode.AUTH_CLIENT_NOT_EXIST) }
val code = OauthUtils.generateCode()

val userIdKey = "$clientId:$code:userId"
val openIdKey = "$clientId:$code:openId"
redisOperation.set(userIdKey, userId, TimeUnit.MINUTES.toSeconds(10L))
if (!nonce.isNullOrBlank()) {
val nonceKey = "$clientId:$code:nonce"
redisOperation.set(nonceKey, nonce, TimeUnit.MINUTES.toSeconds(10L))
}
if (scope.orEmpty().contains("openid")) {
redisOperation.set(openIdKey, true.toString(), TimeUnit.MINUTES.toSeconds(10L))
}
val userIdKey = "$clientId:$code:userId"
val openIdKey = "$clientId:$code:openId"
val expiredInSecond = TimeUnit.MINUTES.toSeconds(10L)
redisOperation.set(userIdKey, userId, expiredInSecond)
if (!nonce.isNullOrBlank()) {
val nonceKey = "$clientId:$code:nonce"
redisOperation.set(nonceKey, nonce!!, expiredInSecond)
}
if (!codeChallenge.isNullOrBlank() && !codeChallengeMethod.isNullOrBlank()) {
val challengeKey = "$clientId:$code:challenge"
redisOperation.set(challengeKey, "${codeChallengeMethod}:${codeChallenge}", expiredInSecond)
}
if (scope.orEmpty().contains("openid")) {
redisOperation.set(openIdKey, true.toString(), expiredInSecond)
}

return AuthorizedResult(
redirectUrl = "${client.redirectUri!!.removeSuffix(StringPool.SLASH)}?code=$code&state=$state",
userId = userId,
appId = client.appId,
scope = client.scope?.toList() ?: emptyList()
)
return AuthorizedResult(
redirectUrl = "${client.redirectUri!!.removeSuffix(StringPool.SLASH)}?code=$code&state=$state",
userId = userId,
appId = client.appId,
scope = client.scope?.toList() ?: emptyList()
)
}
}

override fun createToken(generateTokenRequest: GenerateTokenRequest) {
Expand All @@ -121,14 +130,14 @@ class OauthAuthorizationServiceImpl(
clientId = data.first()
clientSecret = data.last()
}
val code = generateTokenRequest.code
val code = generateTokenRequest.code!!
val userIdKey = "$clientId:$code:userId"
val openIdKey = "$clientId:$code:openId"
val nonceKey = "$clientId:$code:nonce"
val userId = redisOperation.get(userIdKey) ?: throw ErrorCodeException(AuthMessageCode.AUTH_CODE_CHECK_FAILED)
val openId = redisOperation.get(openIdKey).toBoolean()
val nonce = redisOperation.get(nonceKey)
val client = checkClientSecret(clientId, clientSecret)
val client = checkClientSecret(clientId, clientSecret, code, generateTokenRequest.codeVerifier)
var tOauthToken = oauthTokenRepository.findFirstByAccountIdAndUserId(clientId, userId)
val idToken = generateOpenIdToken(clientId, userId, nonce)
if (tOauthToken == null) {
Expand Down Expand Up @@ -161,7 +170,7 @@ class OauthAuthorizationServiceImpl(
with(generateTokenRequest) {
Preconditions.checkNotNull(clientId, this::clientId.name)
Preconditions.checkNotNull(refreshToken, this::refreshToken.name)
checkClientSecret(clientId!!, clientSecret)
checkClientSecret(clientId!!, clientSecret, null, null)
val token = oauthTokenRepository.findFirstByAccountIdAndRefreshToken(clientId!!, refreshToken!!)
?: throw ErrorCodeException(CommonMessageCode.RESOURCE_NOT_FOUND, refreshToken!!)
val idToken = generateOpenIdToken(
Expand Down Expand Up @@ -227,7 +236,7 @@ class OauthAuthorizationServiceImpl(
}

override fun deleteToken(clientId: String, clientSecret: String, accessToken: String) {
checkClientSecret(clientId, clientSecret)
checkClientSecret(clientId, clientSecret, null, null)
oauthTokenRepository.deleteByAccessToken(accessToken)
}

Expand Down Expand Up @@ -289,7 +298,16 @@ class OauthAuthorizationServiceImpl(
)
}

private fun checkClientSecret(clientId: String, clientSecret: String?): TAccount {
private fun checkClientSecret(
clientId: String,
clientSecret: String?,
code: String?,
codeVerifier: String?
): TAccount {
if (clientSecret.isNullOrBlank() && codeVerifier.isNullOrBlank()) {
throw ErrorCodeException(CommonMessageCode.PARAMETER_MISSING, "clientSecret or codeVerifier")
}

val client = accountRepository.findById(clientId)
.orElseThrow { ErrorCodeException(AuthMessageCode.AUTH_CLIENT_NOT_EXIST) }

Expand All @@ -304,9 +322,28 @@ class OauthAuthorizationServiceImpl(
if (credential == null) {
throw ErrorCodeException(AuthMessageCode.AUTH_SECRET_CHECK_FAILED)
}

if (!code.isNullOrBlank()) {
checkCodeVerifier(clientId, code, codeVerifier)
}
return client
}

private fun checkCodeVerifier(clientId: String, code: String, codeVerifier: String?) {
val challengeKey = "$clientId:$code:challenge"
val value = redisOperation.get(challengeKey) ?: return
val (method, challenge) = value.split(StringPool.COLON)
val pass = when (method) {
"plain" -> challenge == codeVerifier
"S256" -> Base64.getUrlEncoder().withoutPadding()
.encodeToString(HashAlgorithm.SHA256().digest(codeVerifier.orEmpty().byteInputStream())) == challenge
else -> false
}
if (!pass) {
throw ErrorCodeException(CommonMessageCode.PARAMETER_INVALID, "code_verifier")
}
}

companion object {
private const val KEY_ID_NAME = "kid"
private const val KEY_ID_VALUE = "bkrepo_rsa_rs256"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ sealed class HashAlgorithm(

fun hash(file: File) = hash(file.inputStream().buffered())

fun hash(inputStream: InputStream): String {
fun digest(inputStream: InputStream): ByteArray {
val digest = MessageDigest.getInstance(algorithm)
inputStream.use {
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
Expand All @@ -54,7 +54,11 @@ sealed class HashAlgorithm(
}
}

val hashBytes = digest.digest()
return digest.digest()
}

fun hash(inputStream: InputStream): String {
val hashBytes = digest(inputStream)
val hashInt = BigInteger(1, hashBytes)
val hashText = hashInt.toString(RADIX)
return if (hashText.length < hashLength)
Expand Down
6 changes: 4 additions & 2 deletions src/frontend/devops-repository/src/store/actions/oauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Vue from 'vue'
const prefix = 'auth/api/oauth'

export default {
getAuthorizeInfo (_, { clientId, state, scope, nonce }) {
getAuthorizeInfo (_, { clientId, state, scope, nonce, codeChallenge, codeChallengeMethod }) {
console.log(clientId, state, scope, nonce)
return Vue.prototype.$ajax.get(
`${prefix}/authorize`,
Expand All @@ -12,7 +12,9 @@ export default {
client_id: clientId,
state,
scope,
nonce
nonce,
code_challenge: codeChallenge,
code_challenge_method: codeChallengeMethod
}
}
)
Expand Down
4 changes: 3 additions & 1 deletion src/frontend/devops-repository/src/views/oauth/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@
clientId: this.$route.query.client_id,
state: this.$route.query.state,
scope: this.$route.query.scope,
nonce: this.$route.query.nonce
nonce: this.$route.query.nonce,
codeChallenge: this.$route.query.code_challenge,
codeChallengeMethod: this.$route.query.code_challenge_method
}).then(authorizeInfo => {
console.log(authorizeInfo.scope.includes('PROJECT'))
this.userId = authorizeInfo.userId
Expand Down

0 comments on commit ef37e05

Please sign in to comment.