PKCE(Proof Key for Code Exchange)とは
概要
PKCEは、OAuth2.0の拡張仕様でPublic Clientに対する「認可コード横取り攻撃」を防ぐためのものです。
https://tools.ietf.org/html/rfc7636
「7. 認可コード横取り攻撃への対抗策 (RFC 7636)」
認可コードフローにより発行された認可コードをクライアントアプリケーションが受け取る際、悪意のあるアプリケーションがその認可コードを横取りするという攻撃 (authorization code interception attack) があります。この攻撃への対抗策として定められた仕様が RFC 7636 (Proof Key for Code Exchange by OAuth Public Clients) です。
クライアント側で実装すべきこと
認証リクエストにcode_challenge_methodとcode_challengeを含め、認証後のトークン取得リクエストにcode_verifierを含めます。
一つは、認可リクエストを投げる際、43 ~ 128 文字の code_verifier を生成して、使用する code_challenge_method のロジック (plain もしくは S256) で code_challenge を計算し、code_challenge_method と code_challenge をリクエストに含めることです。
(中略)
もう一つは、トークンリクエストを投げる際、code_verifier をリクエストに含めることです。
code_challengeの実装手順
- code_verifierの生成
- code_challengeの生成
2-1. SHA256ダイジェストの生成
2-2. BASE64URLエンコード
1. code_verifierの生成
128文字のランダムな英数字をcode_verifierとして生成する。
let codeVerifier = String.randomAlphaNumeric()
extension String {
static func randomAlphaNumeric(_ length: Int = 128) -> String {
let base = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var randomString: String = ""
for _ in 0..<length {
let randomValue = arc4random_uniform(UInt32(base.count))
let index = base.index(base.startIndex, offsetBy: Int(randomValue))
randomString += "\(base[index])"
}
return randomString
}
}
2. code_challengeの生成
let codeChallenge = codeVerifier.sha256().base64URLEncodedString()
2-1. SHA256ダイジェストの生成
SHA256でcode_verifierのハッシュを生成する。
#import <CommonCrypto/CommonHMAC.h>
import Foundation
extension String {
func sha256() -> Data {
guard let data = self.data(using: .utf8) else {
fatalError("data initialize is failed")
}
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes {
_ = CC_SHA256($0, CC_LONG(data.count), &hash)
}
return Data(bytes: hash)
}
}
2-2.BASE64URLエンコード
Dataクラスにbase64EncodedString()はあるが、+と/をURLに使っても問題ないように変換したURL拡張版がないので、自前で置換処理する。
import Foundation
extension Data {
func base64URLEncodedString() -> String {
let base64 = base64EncodedString()
let base64url = String(base64
.dropLast()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_"))
return base64url
}
}
※ SHA256ダイジェスト生成処理を自作した場合のロジックの確認方法
code_verifierがdBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
のときに code_challengeの値がE9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
になるかどうかを確認する。
https://tools.ietf.org/html/rfc7636#appendix-B
let codeVerifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
let codeChallenge = codeVerifier.sha256().base64URLEncodedString()
assert(codeChallenge == "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM")
参考
https://qiita.com/TakahikoKawasaki/items/185d34814eb9f7ac7ef3
https://qiita.com/TakahikoKawasaki/items/00f333c72ed96c4da659
https://qiita.com/SAM-l/items/9574d1e237228c718cd6
https://stackoverflow.com/questions/26845307/generate-random-alphanumeric-string-in-swift/33860834
https://stackoverflow.com/questions/25388747/sha256-in-swift
https://stackoverflow.com/questions/43499651/decode-base64url-to-base64-swift