-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Implemented KMS, JWKS generation and JWT sign #14
Changes from 9 commits
2105846
b3f03c1
14c6a80
6b0a610
d5c34e9
440300c
bfb9cd9
859d788
3fb5bb6
4222e59
bc3ecc8
fe0df4c
db8e116
4848ba3
e6e8527
23545c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package com.sphereon.oid.fed.common.jwt | ||
|
||
expect fun sign(payload: String, opts: Map<String, Any>): String | ||
expect fun verify(jwt: String, key: Any, opts: Map<String, Any>): Boolean |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package com.sphereon.oid.fed.common.jwt | ||
|
||
@JsModule("jose") | ||
@JsNonModule | ||
external object Jose { | ||
class SignJWT { | ||
constructor(payload: dynamic) { | ||
definedExternally | ||
} | ||
fun setProtectedHeader(protectedHeader: dynamic): SignJWT { | ||
definedExternally | ||
} | ||
fun sign(key: Any?, signOptions: Any?): String { | ||
definedExternally | ||
} | ||
} | ||
fun generateKeyPair(alg: String, options: dynamic = definedExternally): dynamic | ||
fun jwtVerify(jwt: String, key: Any, options: dynamic = definedExternally): dynamic | ||
} | ||
|
||
@JsModule("uuid") | ||
@JsNonModule | ||
external object Uuid { | ||
fun v4(): String | ||
} | ||
|
||
@ExperimentalJsExport | ||
@JsExport | ||
actual fun sign( | ||
payload: String, | ||
opts: Map<String, Any> | ||
): String { | ||
val privateKey = opts["privateKey"] ?: throw IllegalArgumentException("JWK private key is required") | ||
val header = opts["jwtHeader"] as String? ?: "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"${Uuid.v4()}\"}" | ||
return Jose.SignJWT(JSON.parse<Any>(payload).asDynamic()) | ||
.setProtectedHeader(JSON.parse<Any>(header).asDynamic()) | ||
.sign(key = privateKey, signOptions = opts) | ||
} | ||
|
||
@ExperimentalJsExport | ||
@JsExport | ||
actual fun verify( | ||
jwt: String, | ||
key: Any, | ||
opts: Map<String, Any> | ||
): Boolean { | ||
return Jose.jwtVerify(jwt, key, opts) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package com.sphereon.oid.fed.common.jwt | ||
|
||
import com.sphereon.oid.fed.common.jwt.Jose.generateKeyPair | ||
import kotlinx.coroutines.async | ||
import kotlinx.coroutines.await | ||
import kotlinx.coroutines.test.runTest | ||
import kotlin.js.Promise | ||
import kotlin.test.Test | ||
import kotlin.test.assertTrue | ||
|
||
class JoseJwtTest { | ||
@OptIn(ExperimentalJsExport::class) | ||
@Test | ||
fun signTest() = runTest { | ||
val keyPair = (generateKeyPair("RS256") as Promise<dynamic>).await() | ||
val result = async { sign("{ \"iss\": \"test\" }", mutableMapOf("privateKey" to keyPair.privateKey)) } | ||
assertTrue((result.await() as Promise<String>).await().startsWith("ey")) | ||
} | ||
|
||
@OptIn(ExperimentalJsExport::class) | ||
@Test | ||
fun verifyTest() = runTest { | ||
val keyPair = (generateKeyPair("RS256") as Promise<dynamic>).await() | ||
val signed = (sign("{ \"iss\": \"test\" }", mutableMapOf("privateKey" to keyPair.privateKey)) as Promise<dynamic>).await() | ||
val result = async { verify(signed, keyPair.publicKey, emptyMap()) } | ||
assertTrue((result.await() as Promise<Boolean>).await()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package com.sphereon.oid.fed.common.jwt | ||
|
||
import com.nimbusds.jose.JWSAlgorithm | ||
import com.nimbusds.jose.JWSHeader | ||
import com.nimbusds.jose.JWSSigner | ||
import com.nimbusds.jose.JWSVerifier | ||
import com.nimbusds.jose.crypto.RSASSASigner | ||
import com.nimbusds.jose.crypto.RSASSAVerifier | ||
import com.nimbusds.jose.jwk.RSAKey | ||
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator | ||
import com.nimbusds.jwt.JWTClaimsSet | ||
import com.nimbusds.jwt.SignedJWT | ||
import java.util.* | ||
|
||
actual fun sign( | ||
payload: String, | ||
opts: Map<String, Any> | ||
): String { | ||
val rsaJWK = opts["key"] as RSAKey? ?: RSAKeyGenerator(2048) | ||
.keyID(UUID.randomUUID().toString()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @nklomp made a comment about not being possible to generate a random kid with uuid There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've made header the second parameter of the sign function, the key should be passed in for both js and jvm now. |
||
.generate() | ||
|
||
val header = opts["jwtHeader"]?.let { | ||
JWSHeader.parse(it as String?) | ||
} ?: JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.keyID).build() | ||
|
||
val signer: JWSSigner = RSASSASigner(rsaJWK) | ||
|
||
val claimsSet = JWTClaimsSet.parse(payload) | ||
|
||
val signedJWT = SignedJWT( | ||
header, | ||
claimsSet | ||
) | ||
|
||
signedJWT.sign(signer) | ||
return signedJWT.serialize() | ||
} | ||
|
||
actual fun verify( | ||
jwt: String, | ||
key: Any, | ||
opts: Map<String, Any> | ||
): Boolean { | ||
try { | ||
val rsaKey = key as RSAKey | ||
val verifier: JWSVerifier = RSASSAVerifier(rsaKey) | ||
val signedJWT = SignedJWT.parse(jwt) | ||
val verified = signedJWT.verify(verifier) | ||
return verified | ||
} catch (e: Exception) { | ||
throw Exception("Couldn't verify the JWT Signature: ${e.message}", e) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package com.sphereon.oid.fed.common.jwt | ||
|
||
import com.nimbusds.jose.jwk.RSAKey | ||
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator | ||
import kotlin.test.Test | ||
import kotlin.test.assertTrue | ||
|
||
class JoseJwtTest { | ||
|
||
@Test | ||
fun signTest() { | ||
val signature = sign("{ \"iss\": \"test\" }", emptyMap()) | ||
assertTrue { signature.startsWith("ey") } | ||
} | ||
|
||
@Test | ||
fun verifyTest() { | ||
val kid = "key1" | ||
val key: RSAKey = RSAKeyGenerator(2048).keyID(kid).generate() | ||
val signature = sign("{ \"iss\": \"test\" }", mutableMapOf( | ||
"key" to key, | ||
"jwtHeader" to "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"${key.keyID}\"}" | ||
)) | ||
assertTrue { verify(signature, key, emptyMap()) } | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should not just create a header, let alone use a random kid value.
Although "kid" is officially not defined, normally they are the JWK thumbprint in case a JWK is used, or a DID VM in case DIDs are used.
So please make a header a first class citizen like the payload. Require it to be passed in. 2nd don't create random kids.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've made header the second parameter of the sign function, the key should be passed in for both js and jvm now.