Skip to content
This repository has been archived by the owner on May 19, 2024. It is now read-only.

Commit

Permalink
[WEAV-64] ✨ security 모듈 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
waterfogSW committed Jan 26, 2024
1 parent 08dd9ba commit bef9b90
Show file tree
Hide file tree
Showing 10 changed files with 456 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.studentcenter.weave.application.common.properties

import com.studentcenter.weave.domain.enum.SocialLoginProvider
import com.studentcenter.weave.support.common.vo.Url
import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(value = "user.jwt")
data class JwtTokenProperties(
val access: TokenProperties,
val refresh: TokenProperties,
val socialLoginProvider: Map<SocialLoginProvider, Provider>,
) {

data class TokenProperties(
val expireSeconds: Long,
val secret: String
)

data class Provider(
val jwksUri: Url,
)

}
3 changes: 3 additions & 0 deletions buildSrc/src/main/kotlin/Version.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ object Version {

const val MYSQL = "8.0.33"
const val H2 = "2.2.224"

const val AUTH0_JAVA_JWT = "4.4.0"
const val AUTH0_JWKS_RSA = "0.22.1"
}
3 changes: 3 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ project(":support:logger").projectDir = file("support/logger")
include(":support:mail")
project(":support:mail").projectDir = file("support/mail")

include(":support:security")
project(":support:security").projectDir = file("support/security")

// domain
include(":domain")
project(":domain").projectDir = file("domain")
Expand Down
4 changes: 4 additions & 0 deletions support/security/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dependencies {
implementation("com.auth0:java-jwt:${Version.AUTH0_JAVA_JWT}")
implementation("com.auth0:jwks-rsa:${Version.AUTH0_JWKS_RSA}")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.studentcenter.weave.support.security.jwt.error

class JwtExpiredException(message: String? = null) : RuntimeException(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.studentcenter.weave.support.security.jwt.error

class JwtVerificationException(message: String? = null) : RuntimeException(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.studentcenter.weave.support.security.jwt.util

import com.auth0.jwk.Jwk
import com.auth0.jwk.JwkProviderBuilder
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.auth0.jwt.exceptions.TokenExpiredException
import com.auth0.jwt.interfaces.DecodedJWT
import com.studentcenter.weave.support.security.jwt.error.JwtExpiredException
import com.studentcenter.weave.support.security.jwt.error.JwtVerificationException
import com.studentcenter.weave.support.security.jwt.vo.JwtClaims
import java.net.URL
import java.security.interfaces.ECPublicKey
import java.security.interfaces.RSAPublicKey
import java.util.concurrent.TimeUnit

object JwtTokenProvider {

fun createToken(
jwtClaims: JwtClaims,
secret: String,
): String {
return JWT
.create()
.withJWTId(jwtClaims.jti)
.withSubject(jwtClaims.sub)
.withIssuer(jwtClaims.iss)
.withAudience(*jwtClaims.aud?.toTypedArray() ?: arrayOf())
.withIssuedAt(jwtClaims.iat)
.withNotBefore(jwtClaims.nbf)
.withExpiresAt(jwtClaims.exp)
.withPayload(jwtClaims.customClaims)
.sign(Algorithm.HMAC256(secret))
}

fun verifyToken(
token: String,
secret: String,
): Result<JwtClaims> {
val algorithm: Algorithm = Algorithm.HMAC256(secret)
return verifyToken(token, algorithm)
}

fun verifyJwksBasedToken(
token: String,
jwksUri: URL,
): Result<JwtClaims> {
val provider = JwkProviderBuilder(jwksUri)
.cached(10, 60 * 24, TimeUnit.HOURS)
.rateLimited(10, 1, TimeUnit.MINUTES)
.build()
val jwt: DecodedJWT = JWT.decode(token)
val jwk: Jwk = provider[jwt.keyId]
val algorithm: Algorithm = extractAlgorithm(jwk)
return verifyToken(token, algorithm)
}

private fun extractAlgorithm(jwk: Jwk): Algorithm {
return when (jwk.algorithm) {
"ES256" -> Algorithm.ECDSA256(jwk.publicKey as ECPublicKey, null)
"ES384" -> Algorithm.ECDSA384(jwk.publicKey as ECPublicKey, null)
"ES512" -> Algorithm.ECDSA512(jwk.publicKey as ECPublicKey, null)
"RS256" -> Algorithm.RSA256(jwk.publicKey as RSAPublicKey, null)
"RS384" -> Algorithm.RSA384(jwk.publicKey as RSAPublicKey, null)
"RS512" -> Algorithm.RSA512(jwk.publicKey as RSAPublicKey, null)
else -> throw IllegalArgumentException("지원되지 않는 알고리즘입니다.")
}
}

private fun verifyToken(
token: String,
algorithm: Algorithm,
): Result<JwtClaims> {
return runCatching {
JWT
.require(algorithm)
.build()
.verify(token)
}.mapCatching { claims ->
JwtClaims.from(claims)
}.onFailure { exception ->
handleException(exception)
}
}

private fun handleException(ex: Throwable): Nothing {
exceptionHandlerList.first { it.first.isAssignableFrom(ex::class.java) }.second.invoke(ex)
}

private val exceptionHandlerList = listOf<Pair<Class<out Throwable>, (Throwable) -> Nothing>>(
TokenExpiredException::class.java to { throw JwtExpiredException("토큰이 만료되었습니다.") },
Throwable::class.java to { e -> throw JwtVerificationException("토큰 유효성 검사에 실패했습니다. message=${e.message}") }
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.studentcenter.weave.support.security.jwt.vo

import com.auth0.jwt.interfaces.DecodedJWT
import java.time.Instant
import java.util.*

/**
* Represents the claims of a JSON Web Token (JWT).
*
* @property registeredClaims The registered claims of the JWT.
* @property customClaims The custom claims of the JWT.
*/
data class JwtClaims(
val registeredClaims: RegisteredClaims,
val customClaims: MutableMap<String, Any> = mutableMapOf()
) {


/**
* Represents the registered claims defined in a JSON Web Token (JWT).
*
* @property sub The subject claim identifying the principal that is the subject of the JWT
* @property exp The expiration time claim that specifies the time on or after which the JWT must not be accepted for processing
* @property iat The issued at claim that identifies the time at which the JWT was issued
* @property nbf The not before claim that identifies the time before which the JWT must not be accepted for processing
* @property iss The issuer claim that identifies the entity that issued the JWT
* @property aud The audience claim that identifies the recipients for which the JWT is intended
* @property jti The JWT ID claim that provides a unique identifier for the JWT
*/
data class RegisteredClaims(
val sub: String? = null,
val exp: Date? = Date.from(Instant.now().plusSeconds(DEFAULT_EXPIRATION_TIME_SEC)),
val iat: Date? = Date.from(Instant.now()),
val nbf: Date? = Date.from(Instant.now()),
val iss: String? = null,
val aud: List<String>? = null,
val jti: String? = null,
)

val sub: String?
get() = registeredClaims.sub
val exp: Date?
get() = registeredClaims.exp
val iat: Date?
get() = registeredClaims.iat
val nbf: Date?
get() = registeredClaims.nbf
val iss: String?
get() = registeredClaims.iss
val aud: List<String>?
get() = registeredClaims.aud
val jti: String?
get() = registeredClaims.jti

companion object {

const val DEFAULT_EXPIRATION_TIME_SEC: Long = 60 * 60 // 1 hour

fun from(claims: DecodedJWT): JwtClaims {
val registeredClaims = RegisteredClaims(
sub = claims.subject,
exp = claims.expiresAt,
iat = claims.issuedAt,
nbf = claims.notBefore,
iss = claims.issuer,
aud = claims.audience,
jti = claims.id,
)
val customClaims: MutableMap<String, Any> = mutableMapOf()
claims.claims
.filter { it.key !in registeredClaims::class.members.map { member -> member.name } }
.forEach { (key, value) ->
customClaims[key] = value.`as`(Any::class.java)
}
return JwtClaims(registeredClaims, customClaims)
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.studentcenter.weave.support.security.jwt.vo

import java.time.Instant
import java.util.*

/**
* Marks the annotated class as a DSL marker for creating JwtClaims objects.
*/
@DslMarker
annotation class JwtClaimsDsl

/**
* Builder class for creating JwtClaims objects.
*/
@JwtClaimsDsl
class JwtClaimsBuilder {
private var registeredClaims = JwtClaims.RegisteredClaims()
private val customClaims = mutableMapOf<String, Any>()

/**
* Sets the registered claims for a JSON Web Token (JWT) using the provided DSL.
* ```
* sub: Subject
* exp: Expiration Time
* iat: Issued At
* nbf: Not Before
* iss: Issuer
* aud: Audience
* jti: JWT ID
* ```
*
* @param init The DSL for setting registered claims.
* @see JwtClaims.registeredClaims
**/
fun registeredClaims(init: RegisteredClaimsBuilder.() -> Unit) {
val builder = RegisteredClaimsBuilder().apply(init)
registeredClaims = builder.build()
}

/**
* Sets the custom claims for a JSON Web Token (JWT) using the provided DSL.
* ```
* this["customKey1"] = "customValue1"
* this["customKey2"] = "customValue2"
* ```
* @param block The DSL for setting custom claims.
* @see JwtClaims.customClaims
*/
fun customClaims(block: MutableMap<String, Any>.() -> Unit) {
customClaims.apply(block)
}

fun build(): JwtClaims = JwtClaims(registeredClaims, customClaims)
}

/**
* A builder class that creates instances of `JwtClaims.RegisteredClaims`.
* The class provides methods to set the registered claims for a JSON Web Token (JWT).
*/
@JwtClaimsDsl
class RegisteredClaimsBuilder {
var sub: String? = null
var exp: Date? = Date.from(Instant.now().plusSeconds(JwtClaims.DEFAULT_EXPIRATION_TIME_SEC))
var iat: Date? = Date.from(Instant.now())
var nbf: Date? = Date.from(Instant.now())
var iss: String? = null
var aud: List<String>? = null
var jti: String? = null

fun build(): JwtClaims.RegisteredClaims =
JwtClaims.RegisteredClaims(sub, exp, iat, nbf, iss, aud, jti)
}

/**
* Creates a new JwtClaims object using the provided DSL.
* ```
* val jwt = JwtClaims {
* registeredClaims {
* sub = "subject"
* // ... Set Registered Claim ...
* }
* customClaims {
* this["customKey1"] = "customValue1"
* // ... Set Custom Claim ...
* }
* }
* ```
* @param init The DSL for creating a JwtClaims object.
* @return A new JwtClaims object.
* @see JwtClaimsBuilder
* @see JwtClaimsDsl
* @see RegisteredClaimsBuilder
* @example
*
*/
fun JwtClaims(init: JwtClaimsBuilder.() -> Unit): JwtClaims = JwtClaimsBuilder().apply(init).build()
Loading

0 comments on commit bef9b90

Please sign in to comment.