JWT#
JSON Web Token(JWT)은 JSON 객체 형태로 당사자 간에 정보를 안전하게 전송하기 위한, 간결하고 자체적으로 완결된 방법을 정의하는 공개 표준(RFC 7519)입니다. 이 정보는 디지털 서명이 되어 있기 때문에 검증하고 신뢰할 수 있습니다.
JWT는 웹 애플리케이션에서 특히 유용하며, 상태를 갖지 않는(stateless) 인증(authentication)/인가(authorization) 및 정보 교환에 흔히 사용됩니다. JWT 이면의 이론에 대해서는 위에 링크된 명세나 jwt.io에서 더 자세히 알아볼 수 있습니다.
Vapor는 JWT 모듈을 통해 JWT에 대한 완전한 지원을 제공합니다. 이 모듈은 SwiftCrypto를 기반으로 한 JWT 표준의 Swift 구현체인 JWTKit 라이브러리 위에 구축되었습니다. JWTKit은 HMAC, ECDSA, EdDSA, RSA 등 다양한 알고리즘에 대한 서명자(signer)와 검증자(verifier)를 제공합니다.
시작하기#
Vapor 애플리케이션에서 JWT를 사용하기 위한 첫 번째 단계는 프로젝트의 Package.swift 파일에 JWT 종속성을 추가하는 것입니다.
// swift-tools-version:5.10
import PackageDescription
let package = Package(
name: "my-app",
dependencies: [
// Other dependencies...
.package(url: "https://github.com/vapor/jwt.git", from: "5.0.0"),
],
targets: [
.target(name: "App", dependencies: [
// Other dependencies...
.product(name: "JWT", package: "jwt")
]),
// Other targets...
]
)
설정#
종속성을 추가한 뒤에는 애플리케이션에서 JWT 모듈을 사용할 수 있습니다. JWT 모듈은 Application에 설정을 위한 새로운 jwt 프로퍼티를 추가하며, 그 내부는 JWTKit 라이브러리에 의해 제공됩니다.
키 컬렉션(Key Collection)#
jwt 객체에는 JWTKit의 JWTKeyCollection 인스턴스인 keys 프로퍼티가 있습니다. 이 컬렉션은 JWT를 서명하고 검증하는 데 사용되는 키를 저장하고 관리하는 데 사용됩니다. JWTKeyCollection은 actor이므로 컬렉션에 대한 모든 작업이 직렬화되어 스레드 안전(thread-safe)합니다.
JWT를 서명하거나 검증하려면 컬렉션에 키를 추가해야 합니다. 이는 보통 configure.swift 파일에서 이루어집니다.
import JWT
// Add HMAC with SHA-256 signer.
await app.jwt.keys.add(hmac: "secret", digestAlgorithm: .sha256)
이렇게 하면 다이제스트 알고리즘으로 SHA-256을 사용하는 HMAC 키(JWA 표기법으로는 HS256)가 키체인에 추가됩니다. 사용 가능한 알고리즘에 대한 자세한 내용은 아래의 알고리즘 섹션을 확인하세요.
Note
"secret"을 실제 비밀 키로 반드시 교체하세요. 이 키는 이상적으로는 설정 파일이나 환경 변수에 안전하게 보관해야 합니다.
서명하기#
추가한 키는 JWT에 서명하는 데 사용할 수 있습니다. 이를 위해서는 먼저 서명할 대상, 즉 ’페이로드(payload)’가 필요합니다.
이 페이로드는 여러분이 전송하고자 하는 데이터를 담고 있는 단순한 JSON 객체입니다. 구조체를 JWTPayload 프로토콜에 준수시켜 사용자 정의 페이로드를 만들 수 있습니다.
// JWT payload structure.
struct TestPayload: JWTPayload {
// Maps the longer Swift property names to the
// shortened keys used in the JWT payload.
enum CodingKeys: String, CodingKey {
case subject = "sub"
case expiration = "exp"
case isAdmin = "admin"
}
// The "sub" (subject) claim identifies the principal that is the
// subject of the JWT.
var subject: SubjectClaim
// The "exp" (expiration time) claim identifies the expiration time on
// or after which the JWT MUST NOT be accepted for processing.
var expiration: ExpirationClaim
// Custom data.
// If true, the user is an admin.
var isAdmin: Bool
// Run any additional verification logic beyond
// signature verification here.
// Since we have an ExpirationClaim, we will
// call its verify method.
func verify(using algorithm: some JWTAlgorithm) async throws {
try self.expiration.verifyNotExpired()
}
}
페이로드에 서명하려면 라우트 핸들러 내부 등에서 JWT 모듈의 sign 메서드를 호출하면 됩니다.
app.post("login") { req async throws -> [String: String] in
let payload = TestPayload(
subject: "vapor",
expiration: .init(value: .distantFuture),
isAdmin: true
)
return try await ["token": req.jwt.sign(payload)]
}
이 엔드포인트로 요청을 보내면 서명된 JWT가 응답 본문에 String으로 반환되며, 모든 것이 계획대로 진행되었다면 다음과 같은 결과를 보게 됩니다.
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo"
}
이 토큰은 jwt.io 디버거를 사용하여 디코딩하고 검증할 수 있습니다. 이 디버거는 JWT의 페이로드(앞서 지정한 데이터가 보여야 합니다)와 헤더를 보여주며, JWT 서명에 사용한 비밀 키를 이용해 서명을 검증할 수 있습니다.
검증하기#
반대로 애플리케이션에 토큰이 전송되었을 때는, JWT 모듈의 verify 메서드를 호출하여 토큰의 진위를 검증할 수 있습니다.
// Fetch and verify JWT from incoming request.
app.get("me") { req async throws -> HTTPStatus in
let payload = try await req.jwt.verify(as: TestPayload.self)
print(payload)
return .ok
}
req.jwt.verify 헬퍼는 Authorization 헤더에서 bearer 토큰을 확인합니다. 토큰이 존재하면 JWT를 파싱하고 서명과 클레임을 검증합니다. 이 단계 중 어느 하나라도 실패하면 401 Unauthorized 에러가 발생합니다.
다음 HTTP 요청을 보내서 라우트를 테스트해보세요.
GET /me HTTP/1.1
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo
모든 것이 잘 작동했다면 200 OK 응답이 반환되고 페이로드가 출력됩니다.
TestPayload(
subject: "vapor",
expiration: 4001-01-01 00:00:00 +0000,
isAdmin: true
)
전체 인증 흐름은 인증 → JWT에서 확인할 수 있습니다.
알고리즘#
JWT는 다양한 알고리즘을 사용하여 서명할 수 있습니다.
키체인에 키를 추가하려면 다음 각 알고리즘에 대한 add 메서드의 오버로드를 사용할 수 있습니다.
HMAC#
HMAC(Hash-based Message Authentication Code)는 JWT를 서명하고 검증하는 데 비밀 키를 사용하는 대칭 알고리즘입니다. Vapor는 다음 HMAC 알고리즘을 지원합니다.
HS256: SHA-256을 사용하는 HMACHS384: SHA-384를 사용하는 HMACHS512: SHA-512를 사용하는 HMAC
// Add an HS256 key.
await app.jwt.keys.add(hmac: "secret", digestAlgorithm: .sha256)
ECDSA#
ECDSA(Elliptic Curve Digital Signature Algorithm)는 JWT를 서명하고 검증하는 데 공개/개인 키 쌍을 사용하는 비대칭 알고리즘입니다. 이는 타원 곡선에 관한 수학을 기반으로 합니다. Vapor는 다음 ECDSA 알고리즘을 지원합니다.
ES256: P-256 곡선과 SHA-256을 사용하는 ECDSAES384: P-384 곡선과 SHA-384를 사용하는 ECDSAES512: P-521 곡선과 SHA-512를 사용하는 ECDSA
모든 알고리즘은 ES256PublicKey, ES256PrivateKey와 같이 공개 키와 개인 키를 모두 제공합니다. PEM 형식을 사용하여 ECDSA 키를 추가할 수 있습니다.
let ecdsaPublicKey = """
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2adMrdG7aUfZH57aeKFFM01dPnkx
C18ScRb4Z6poMBgJtYlVtd9ly63URv57ZW0Ncs1LiZB7WATb3svu+1c7HQ==
-----END PUBLIC KEY-----
"""
// Initialize an ECDSA key with public PEM.
let key = try ES256PublicKey(pem: ecdsaPublicKey)
또는 (테스트에 유용한) 임의의 키를 생성할 수 있습니다.
let key = ES256PrivateKey()
키체인에 키를 추가하려면 다음과 같이 합니다.
await app.jwt.keys.add(ecdsa: key)
EdDSA#
EdDSA(Edwards-curve Digital Signature Algorithm)는 JWT를 서명하고 검증하는 데 공개/개인 키 쌍을 사용하는 비대칭 알고리즘입니다. 둘 다 DSA 알고리즘에 기반한다는 점에서 ECDSA와 비슷하지만, EdDSA는 타원 곡선의 또 다른 계열인 Edwards 곡선을 기반으로 하며 성능이 약간 더 좋습니다. 다만 더 새로운 알고리즘이기 때문에 지원 범위가 상대적으로 좁습니다. Vapor는 Ed25519 곡선을 사용하는 EdDSA 알고리즘만 지원합니다.
EdDSA 키는 (base-64로 인코딩된 String) 좌표를 사용하여 만들 수 있는데, 공개 키라면 x를, 개인 키라면 d를 사용합니다.
let publicKey = try EdDSA.PublicKey(x: "0ZcEvMCSYqSwR8XIkxOoaYjRQSAO8frTMSCpNbUl4lE", curve: .ed25519)
let privateKey = try EdDSA.PrivateKey(d: "d1H3/dcg0V3XyAuZW2TE5Z3rhY20M+4YAfYu/HUQd8w=", curve: .ed25519)
또한 임의의 키를 생성할 수도 있습니다.
let key = EdDSA.PrivateKey(curve: .ed25519)
키체인에 키를 추가하려면 다음과 같이 합니다.
await app.jwt.keys.add(eddsa: key)
RSA#
RSA(Rivest-Shamir-Adleman)는 JWT를 서명하고 검증하는 데 공개/개인 키 쌍을 사용하는 비대칭 알고리즘입니다.
Warning
보시다시피 RSA 키는 새로운 사용자가 사용하지 않도록 유도하기 위해 Insecure 네임스페이스 뒤에 배치되어 있습니다. 이는 RSA가 ECDSA 및 EdDSA보다 덜 안전하다고 여겨지기 때문이며, 호환성을 위한 이유가 있을 때만 사용해야 합니다.
가능하다면 다른 알고리즘 중 하나를 대신 사용하세요.
Vapor는 다음 RSA 알고리즘을 지원합니다.
RS256: SHA-256을 사용하는 RSARS384: SHA-384를 사용하는 RSARS512: SHA-512를 사용하는 RSA
PEM 형식을 사용하여 RSA 키를 만들 수 있습니다.
let rsaPublicKey = """
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0cOtPjzABybjzm3fCg1aCYwnx
PmjXpbCkecAWLj/CcDWEcuTZkYDiSG0zgglbbbhcV0vJQDWSv60tnlA3cjSYutAv
7FPo5Cq8FkvrdDzeacwRSxYuIq1LtYnd6I30qNaNthntjvbqyMmBulJ1mzLI+Xg/
aX4rbSL49Z3dAQn8vQIDAQAB
-----END PUBLIC KEY-----
"""
// Initialize an RSA key with public pem.
let key = try Insecure.RSA.PublicKey(pem: rsaPublicKey)
또는 구성 요소를 사용해서 만들 수도 있습니다.
// Initialize an RSA private key with components.
let key = try Insecure.RSA.PrivateKey(
modulus: modulus,
exponent: publicExponent,
privateExponent: privateExponent
)
Warning
이 패키지는 2048비트보다 작은 RSA 키를 지원하지 않습니다.
그런 다음 키 컬렉션에 키를 추가할 수 있습니다.
await app.jwt.keys.add(rsa: key, digestAlgorithm: .sha256)
PSS#
Vapor는 RSA-PKCS1v1.5 알고리즘 외에도 RSA-PSS 알고리즘을 지원합니다. PSS(Probabilistic Signature Scheme)는 RSA 서명을 위한 더 안전한 패딩 방식입니다. 가능하다면 PKCS1v1.5보다 PSS를 사용하는 것이 권장됩니다.
이 알고리즘은 서명 단계에서만 차이가 있으므로 키는 RSA와 동일하지만, 키 컬렉션에 추가할 때 패딩 방식을 지정해야 합니다.
await app.jwt.keys.add(pss: key, digestAlgorithm: .sha256)
키 식별자(kid)#
키 컬렉션에 키를 추가할 때 키 식별자(kid)를 지정할 수도 있습니다. 이는 컬렉션에서 키를 조회하는 데 사용할 수 있는, 해당 키에 대한 고유 식별자입니다.
// Add HMAC with SHA-256 key named "a".
await app.jwt.keys.add(hmac: "foo", digestAlgorithm: .sha256, kid: "a")
kid를 지정하지 않으면 해당 키는 기본 키로 지정됩니다.
Note
kid 없이 다른 키를 추가하면 기본 키가 재정의(override)됩니다.
JWT에 서명할 때 사용할 kid를 지정할 수 있습니다.
let token = try await req.jwt.sign(payload, kid: "a")
반면 검증할 때는 kid가 JWT 헤더에서 자동으로 추출되어 컬렉션에서 키를 조회하는 데 사용됩니다. verify 메서드에는 kid를 찾지 못했을 때 컬렉션의 모든 키를 순회할지 여부를 지정할 수 있는 iteratingKeys 매개변수도 있습니다.
클레임(Claims)#
Vapor의 JWT 패키지는 일반적인 JWT 클레임을 구현하기 위한 여러 헬퍼를 포함하고 있습니다.
| 클레임 | 타입 | 검증 메서드 |
|---|---|---|
aud |
AudienceClaim |
verifyIntendedAudience(includes:) |
exp |
ExpirationClaim |
verifyNotExpired(currentDate:) |
jti |
IDClaim |
n/a |
iat |
IssuedAtClaim |
n/a |
iss |
IssuerClaim |
n/a |
locale |
LocaleClaim |
n/a |
nbf |
NotBeforeClaim |
verifyNotBefore(currentDate:) |
sub |
SubjectClaim |
n/a |
모든 클레임은 JWTPayload.verify 메서드에서 검증해야 합니다. 클레임에 전용 검증 메서드가 있다면 그것을 사용하면 됩니다. 그렇지 않다면 value를 사용하여 클레임의 값에 접근하고 유효한지 확인하세요.
JWK#
JSON Web Key(JWK)는 암호화 키를 나타내는 JSON 데이터 구조(RFC7517)입니다. 이는 JWT를 검증하기 위한 키를 클라이언트에 제공하는 데 흔히 사용됩니다.
예를 들어 Apple은 다음 URL에서 Sign in with Apple JWKS를 호스팅합니다.
GET https://appleid.apple.com/auth/keys
Vapor는 키 컬렉션에 JWK를 추가하는 유틸리티를 제공합니다.
let privateKey = """
{
"kty": "RSA",
"d": "\(rsaPrivateExponent)",
"e": "AQAB",
"use": "sig",
"kid": "1234",
"alg": "RS256",
"n": "\(rsaModulus)"
}
"""
let jwk = try JWK(json: privateKey)
try await app.jwt.keys.use(jwk: jwk)
이렇게 하면 JWK가 키 컬렉션에 추가되며, 다른 키와 마찬가지로 이를 사용해 JWT를 서명하고 검증할 수 있습니다.
JWK 목록#
여러 개의 JWK가 있다면 마찬가지로 추가할 수 있습니다.
let json = """
{
"keys": [
{"kty": "RSA", "alg": "RS256", "kid": "a", "n": "\(rsaModulus)", "e": "AQAB"},
{"kty": "RSA", "alg": "RS512", "kid": "b", "n": "\(rsaModulus)", "e": "AQAB"},
]
}
"""
try await app.jwt.keys.use(jwksJSON: json)
발급 기관(Vendors)#
Vapor는 아래의 인기 있는 발급 기관(issuer)으로부터의 JWT를 처리하기 위한 API를 제공합니다.
Apple#
먼저 Apple 애플리케이션 식별자를 설정하세요.
// Configure Apple app identifier.
app.jwt.apple.applicationIdentifier = "..."
그런 다음 req.jwt.apple 헬퍼를 사용하여 Apple JWT를 가져와 검증합니다.
// Fetch and verify Apple JWT from Authorization header.
app.get("apple") { req async throws -> HTTPStatus in
let token = try await req.jwt.apple.verify()
print(token) // AppleIdentityToken
return .ok
}
Google#
먼저 Google 애플리케이션 식별자와 G Suite 도메인 이름을 설정하세요.
// Configure Google app identifier and domain name.
app.jwt.google.applicationIdentifier = "..."
app.jwt.google.gSuiteDomainName = "..."
그런 다음 req.jwt.google 헬퍼를 사용하여 Google JWT를 가져와 검증합니다.
// Fetch and verify Google JWT from Authorization header.
app.get("google") { req async throws -> HTTPStatus in
let token = try await req.jwt.google.verify()
print(token) // GoogleIdentityToken
return .ok
}
Microsoft#
먼저 Microsoft 애플리케이션 식별자를 설정하세요.
// Configure Microsoft app identifier.
app.jwt.microsoft.applicationIdentifier = "..."
그런 다음 req.jwt.microsoft 헬퍼를 사용하여 Microsoft JWT를 가져와 검증합니다.
// Fetch and verify Microsoft JWT from Authorization header.
app.get("microsoft") { req async throws -> HTTPStatus in
let token = try await req.jwt.microsoft.verify()
print(token) // MicrosoftIdentityToken
return .ok
}