Sign in with apple server side
- Client에서 구현된 oauth webview or login sdk 를 활용하여 signin with apple
authorization_code
획득- sign request with
authorization_code
Server side
authorization_code
를 통해 token 생성 요청TokenResponse
TokenResponse
validateTokenResponse
에서 UserInfo 추출
4~7 과정의 내용을 정리
사전준비
- JWT 관련 기능은 auth0 library를 사용
- Certificates, Identifiers & Profiles
- team-id
- client-id
- key-id
- private key file
- 모두 Apple developer console에서 획득 가능하다
주의사항
authorization_code
is single-use only
server-side authenticate이므로 Token verification 구현은 생략해도되나 참고용으로 작성
결과
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
class ApplePublicKeys(
val keys: Array<ApplePublicKey>
)
data class ApplePublicKey(
val alg: String,
val e: String,
val kid: String,
val kty: String,
val n: String,
val use: String
)
data class TokenResponse(
val accessToken: String,
val expiresIn: Int,
val idToken: String,
val refreshToken: String,
val tokenType: String
)
/**
* Token generate에 사용할 client-secret은 ECDSA (비대칭 암호화) algorithm을 사용하여
* 생성해야된다.
*/
class ExampleKeyProvider : ECDSAKeyProvider {
private val PEM_URI = ""
/**
* singing 과정만 필요하므로 private key만 구현
*/
override fun getPublicKeyById(keyId: String?): ECPublicKey? = null
override fun getPrivateKey(): ECPrivateKey {
val file = ResourceUtils.getFile(PEM_URI)
PemReader(FileReader(file)).use { reader ->
val content = reader.readPemObject().content
return KeyFactory.getInstance("EC")
.generatePrivate(PKCS8EncodedKeySpec(content)) as ECPrivateKey
}
}
override fun getPrivateKeyId(): String? = null
}
object AppleAuthExample {
private val rest by lazy {
RestTemplate()
}
private val CLIENT_ID = ""
private val TEAM_ID = ""
private val KEY_ID = ""
private const val AUTH_URL = "https://appleid.apple.com"
fun createClientSecret(): String? {
val now = Date()
return JWT.create()
.withHeader(
mapOf(
"kid" to KEY_ID
)
)
.withSubject(CLIENT_ID)
.withIssuer(TEAM_ID)
.withIssuedAt(now)
.withExpiresAt(Date(now.time + 1.hours.inWholeMilliseconds))
.withAudience(AUTH_URL)
.sign(Algorithm.ECDSA256(ExampleKeyProvider()))
}
fun authenticate(authCode: String) {
val headers = HttpHeaders().apply {
contentType = MediaType.APPLICATION_FORM_URLENCODED
}
val map: MultiValueMap<String, String> = LinkedMultiValueMap<String, String>().apply {
add("client_id", CLIENT_ID)
add("client_secret", createClientSecret())
add("grant_type", "authorization_code")
add("code", authCode)
}
val entity = HttpEntity(map, headers)
//authorization_code를 통해 token을 생성한다
val idToken = rest.exchange(
"https://appleid.apple.com/auth/token",
HttpMethod.POST,
entity,
TokenResponse::class.java
).body?.idToken ?: throw RuntimeException("invalid or revoked authorization_code")
val jwt = decodeIdToken(idToken) ?: throw RuntimeException("invalid idToken")
if (!verifyToken(jwt)) throw RuntimeException("invalid idToken")
/**
* TODO: handle singing
* jwt.subject is unique userId
* jwt.getClaim("email") is user email
*/
}
//Verify the JWS E256 signature using the server’s public key
private fun decodeIdToken(token: String?): DecodedJWT? {
val keys =
rest.getForEntity("https://appleid.apple.com/auth/keys", ApplePublicKeys::class.java).body?.keys
?: return null
keys.forEach {
val nBytes: ByteArray = Base64.getUrlDecoder().decode(it.n)
val eBytes: ByteArray = Base64.getUrlDecoder().decode(it.e)
val modules = BigInteger(1, nBytes)
val exponent = BigInteger(1, eBytes)
val spec = RSAPublicKeySpec(modules, exponent)
val kf: KeyFactory = KeyFactory.getInstance("RSA")
val publicKey: RSAPublicKey = kf.generatePublic(spec) as RSAPublicKey
try {
return JWT.require(
Algorithm.RSA256(
publicKey, null
)
).build().verify(token)
} catch (e: Exception) {
}
}
return null
}
/**
* https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/verifying_a_user
*/
private fun verifyToken(token: DecodedJWT): Boolean {
//Verify that the time is earlier than the exp value of the token
val verifyTime = Date() < token.expiresAt
//Verify that the aud field is the developer’s client_id
val verifyAud = token.audience.firstOrNull() == CLIENT_ID
//Verify that the iss field contains https://appleid.apple.com
val verifyIssuer = token.issuer == AUTH_URL
return verifyTime && verifyAud && verifyIssuer
}
}