rely on zod to remove os specific restraints
This commit is contained in:
@@ -14,7 +14,7 @@ class CryptoUtil {
|
|||||||
return Data(bytes)
|
return Data(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func generateKeyPair() throws -> JWKPair? {
|
public static func generateKeyPair() throws -> [String:String] {
|
||||||
let keyIdString = UUID().uuidString
|
let keyIdString = UUID().uuidString
|
||||||
|
|
||||||
let privateKey = P256.Signing.PrivateKey()
|
let privateKey = P256.Signing.PrivateKey()
|
||||||
@@ -24,27 +24,13 @@ class CryptoUtil {
|
|||||||
let y = publicKey.x963Representation[33...].base64URLEncodedString()
|
let y = publicKey.x963Representation[33...].base64URLEncodedString()
|
||||||
let d = privateKey.rawRepresentation.base64URLEncodedString()
|
let d = privateKey.rawRepresentation.base64URLEncodedString()
|
||||||
|
|
||||||
let publicJWK = JWK(
|
let publicJWK = JWK(kty: "EC", use: "sig", crv: "P-256", kid: keyIdString, x: x, y: y, alg: "ES256")
|
||||||
alg: "ES256".toField(),
|
let privateJWK = JWK(kty: "EC", use: "sig", crv: "P-256", kid: keyIdString, x: x, y: y, d: d, alg: "ES256")
|
||||||
kty: "EC".toField(),
|
|
||||||
crv: "P-256".toNullableField(),
|
|
||||||
x: x.toNullableField(),
|
|
||||||
y: y.toNullableField(),
|
|
||||||
use: "sig".toNullableField(),
|
|
||||||
kid: keyIdString.toNullableField()
|
|
||||||
)
|
|
||||||
let privateJWK = JWK(
|
|
||||||
alg: "ES256".toField(),
|
|
||||||
kty: "EC".toField(),
|
|
||||||
crv: "P-256".toNullableField(),
|
|
||||||
x: x.toNullableField(),
|
|
||||||
y: y.toNullableField(),
|
|
||||||
d: d.toNullableField(),
|
|
||||||
use: "sig".toNullableField(),
|
|
||||||
kid: keyIdString.toNullableField()
|
|
||||||
)
|
|
||||||
|
|
||||||
return JWKPair(privateKey: privateJWK.toField(), publicKey: publicJWK.toField())
|
return [
|
||||||
|
"privateKey": privateJWK.toJson(),
|
||||||
|
"publicKey": publicJWK.toJson()
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public class ExpoBlueskyOAuthClientModule: Module {
|
|||||||
promise.resolve(keypair)
|
promise.resolve(keypair)
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("createJwt") { (header: JWTHeader, payload: JWTPayload, jwk: JWK, promise: Promise) in
|
AsyncFunction("createJwt") { (header: String, payload: String, jwk: String, promise: Promise) in
|
||||||
guard let jwt = JWTUtil.createJwt(header: header, payload: payload, jwk: jwk) else {
|
guard let jwt = JWTUtil.createJwt(header: header, payload: payload, jwk: jwk) else {
|
||||||
promise.reject("JWTError", "Error creating JWT.")
|
promise.reject("JWTError", "Error creating JWT.")
|
||||||
return
|
return
|
||||||
@@ -42,7 +42,7 @@ public class ExpoBlueskyOAuthClientModule: Module {
|
|||||||
promise.resolve(jwt)
|
promise.resolve(jwt)
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("verifyJwt") { (token: String, jwk: JWK, promise: Promise) in
|
AsyncFunction("verifyJwt") { (token: String, jwk: String, promise: Promise) in
|
||||||
promise.resolve(JWTUtil.verifyJwt(token: token, jwk: jwk))
|
promise.resolve(JWTUtil.verifyJwt(token: token, jwk: jwk))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,32 @@
|
|||||||
import ExpoModulesCore
|
import ExpoModulesCore
|
||||||
import JOSESwift
|
import JOSESwift
|
||||||
|
|
||||||
struct JWK : Record {
|
struct JWK {
|
||||||
@Field
|
let kty: String
|
||||||
var alg: String
|
let use: String
|
||||||
@Field
|
let crv: String
|
||||||
var kty: String
|
let kid: String
|
||||||
@Field
|
let x: String
|
||||||
var crv: String?
|
let y: String
|
||||||
@Field
|
|
||||||
var x: String?
|
|
||||||
@Field
|
|
||||||
var y: String?
|
|
||||||
@Field
|
|
||||||
var e: String?
|
|
||||||
@Field
|
|
||||||
var n: String?
|
|
||||||
@Field
|
|
||||||
var d: String?
|
var d: String?
|
||||||
@Field
|
let alg: String
|
||||||
var use: String?
|
|
||||||
@Field
|
|
||||||
var kid: String?
|
|
||||||
|
|
||||||
func toField() -> Field<JWK> {
|
func toJson() -> String {
|
||||||
return Field(wrappedValue: self)
|
var dict: [String: Any] = [
|
||||||
|
"kty": kty,
|
||||||
|
"use": use,
|
||||||
|
"crv": crv,
|
||||||
|
"kid": kid,
|
||||||
|
"x": x,
|
||||||
|
"y": y,
|
||||||
|
"alg": alg,
|
||||||
|
]
|
||||||
|
|
||||||
|
if let d = d {
|
||||||
|
dict["d"] = d
|
||||||
}
|
}
|
||||||
|
|
||||||
func toPrivateSecKey() throws -> SecKey? {
|
let jsonData = try! JSONSerialization.data(withJSONObject: dict, options: [])
|
||||||
let jsonData = try JSONSerialization.data(withJSONObject: self.toDictionary())
|
return String(data: jsonData, encoding: .utf8)!
|
||||||
guard let jwk = try? JSONDecoder().decode(ECPrivateKey.self, from: jsonData),
|
|
||||||
let key = try? jwk.converted(to: SecKey.self)
|
|
||||||
else {
|
|
||||||
print("Error creating SecKey.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
func toPublicSecKey() throws -> SecKey? {
|
|
||||||
let jsonData = try JSONSerialization.data(withJSONObject: self.toDictionary())
|
|
||||||
guard let jwk = try? JSONDecoder().decode(ECPublicKey.self, from: jsonData),
|
|
||||||
let key = try? jwk.converted(to: SecKey.self)
|
|
||||||
else {
|
|
||||||
print("Error creating SecKey.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct JWKPair : Record {
|
|
||||||
@Field
|
|
||||||
var privateKey: JWK
|
|
||||||
@Field
|
|
||||||
var publicKey: JWK
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,184 +1,9 @@
|
|||||||
import ExpoModulesCore
|
import ExpoModulesCore
|
||||||
import JOSESwift
|
import JOSESwift
|
||||||
|
|
||||||
struct JWTHeader : Record {
|
|
||||||
@Field
|
|
||||||
var alg: String = "ES256"
|
|
||||||
@Field
|
|
||||||
var jku: String?
|
|
||||||
@Field
|
|
||||||
var jwk: JWK
|
|
||||||
@Field
|
|
||||||
var kid: String?
|
|
||||||
@Field
|
|
||||||
var typ: String?
|
|
||||||
@Field
|
|
||||||
var cty: String?
|
|
||||||
@Field
|
|
||||||
var crit: String?
|
|
||||||
|
|
||||||
func toField() -> Field<JWTHeader> {
|
|
||||||
return Field(wrappedValue: self)
|
|
||||||
}
|
|
||||||
|
|
||||||
func toJWSHeader() throws -> JWSHeader? {
|
|
||||||
return JWSHeader(try JSONSerialization.data(withJSONObject: self.toDictionary()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct JWTPayload : Record {
|
|
||||||
@Field
|
|
||||||
var iss: String?
|
|
||||||
@Field
|
|
||||||
var aud: String?
|
|
||||||
@Field
|
|
||||||
var sub: String?
|
|
||||||
@Field
|
|
||||||
var exp: Int?
|
|
||||||
@Field
|
|
||||||
var nbr: Int?
|
|
||||||
@Field
|
|
||||||
var iat: Int?
|
|
||||||
@Field
|
|
||||||
var jti: String?
|
|
||||||
@Field
|
|
||||||
var htm: String?
|
|
||||||
@Field
|
|
||||||
var htu: String?
|
|
||||||
@Field
|
|
||||||
var ath: String?
|
|
||||||
@Field
|
|
||||||
var acr: String?
|
|
||||||
@Field
|
|
||||||
var azp: String?
|
|
||||||
@Field
|
|
||||||
var amr: String?
|
|
||||||
@Field
|
|
||||||
var cnf: JWTPayloadCNF?
|
|
||||||
@Field
|
|
||||||
var client_id: String?
|
|
||||||
@Field
|
|
||||||
var scope: String?
|
|
||||||
@Field
|
|
||||||
var nonce: String?
|
|
||||||
@Field
|
|
||||||
var at_hash: String?
|
|
||||||
@Field
|
|
||||||
var c_hash: String?
|
|
||||||
@Field
|
|
||||||
var s_hash: String?
|
|
||||||
@Field
|
|
||||||
var auth_time: Int?
|
|
||||||
@Field
|
|
||||||
var name: String?
|
|
||||||
@Field
|
|
||||||
var family_name: String?
|
|
||||||
@Field
|
|
||||||
var given_name: String?
|
|
||||||
@Field
|
|
||||||
var middle_name: String?
|
|
||||||
@Field
|
|
||||||
var nickname: String?
|
|
||||||
@Field
|
|
||||||
var preferred_username: String?
|
|
||||||
@Field
|
|
||||||
var gender: String?
|
|
||||||
@Field
|
|
||||||
var picture: String?
|
|
||||||
@Field
|
|
||||||
var profile: String?
|
|
||||||
@Field
|
|
||||||
var website: String?
|
|
||||||
@Field
|
|
||||||
var birthdate: String?
|
|
||||||
@Field
|
|
||||||
var zoneinfo: String?
|
|
||||||
@Field
|
|
||||||
var locale: String?
|
|
||||||
@Field
|
|
||||||
var updated_at: Int?
|
|
||||||
@Field
|
|
||||||
var email: String?
|
|
||||||
@Field
|
|
||||||
var email_verified: Bool?
|
|
||||||
@Field
|
|
||||||
var phone_number: String?
|
|
||||||
@Field
|
|
||||||
var phone_number_verified: Bool?
|
|
||||||
@Field
|
|
||||||
var address: JWTPayloadAddress?
|
|
||||||
@Field
|
|
||||||
var authorization_details: JWTPayloadAuthorizationDetails?
|
|
||||||
|
|
||||||
func toField() -> Field<JWTPayload> {
|
|
||||||
return Field(wrappedValue: self)
|
|
||||||
}
|
|
||||||
|
|
||||||
func toPayload() throws -> Payload {
|
|
||||||
return Payload(try JSONSerialization.data(withJSONObject: self.toDictionary()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct JWTPayloadCNF : Record {
|
|
||||||
@Field
|
|
||||||
var kid: String?
|
|
||||||
@Field
|
|
||||||
var jwk: JWK?
|
|
||||||
@Field
|
|
||||||
var jwe: String?
|
|
||||||
@Field
|
|
||||||
var jku: String?
|
|
||||||
@Field
|
|
||||||
var jkt: String?
|
|
||||||
@Field
|
|
||||||
var osc: String?
|
|
||||||
|
|
||||||
func toField() -> Field<JWTPayloadCNF> {
|
|
||||||
return Field(wrappedValue: self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct JWTPayloadAddress : Record {
|
|
||||||
@Field
|
|
||||||
var formatted: String?
|
|
||||||
@Field
|
|
||||||
var street_address: String?
|
|
||||||
@Field
|
|
||||||
var locality: String?
|
|
||||||
@Field
|
|
||||||
var region: String?
|
|
||||||
@Field
|
|
||||||
var postal_code: String?
|
|
||||||
@Field
|
|
||||||
var country: String?
|
|
||||||
|
|
||||||
func toField() -> Field<JWTPayloadAddress> {
|
|
||||||
return Field(wrappedValue: self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct JWTPayloadAuthorizationDetails : Record {
|
|
||||||
@Field
|
|
||||||
var type: String
|
|
||||||
@Field
|
|
||||||
var locations: [String]?
|
|
||||||
@Field
|
|
||||||
var actions: [String]?
|
|
||||||
@Field
|
|
||||||
var datatypes: [String]?
|
|
||||||
@Field
|
|
||||||
var identifier: String?
|
|
||||||
@Field
|
|
||||||
var privileges: [String]?
|
|
||||||
|
|
||||||
func toField() -> Field<JWTPayloadAuthorizationDetails> {
|
|
||||||
return Field(wrappedValue: self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct JWTVerifyResponse : Record {
|
struct JWTVerifyResponse : Record {
|
||||||
@Field
|
@Field
|
||||||
var protectedHeader: JWTHeader
|
var payload: String?
|
||||||
@Field
|
@Field
|
||||||
var payload: String
|
var protectedHeader: String?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,18 @@ import ExpoModulesCore
|
|||||||
import JOSESwift
|
import JOSESwift
|
||||||
|
|
||||||
class JWTUtil {
|
class JWTUtil {
|
||||||
|
static func jsonToPrivateKey(_ jwkString: String) throws -> SecKey? {
|
||||||
|
guard let jsonData = jwkString.data(using: .utf8),
|
||||||
|
let jwk = try? JSONDecoder().decode(ECPrivateKey.self, from: jsonData),
|
||||||
|
let key = try? jwk.converted(to: SecKey.self)
|
||||||
|
else {
|
||||||
|
print("Error creating JWK from JWK string.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
static func jsonToPublicKey(_ jwkString: String) throws -> SecKey? {
|
static func jsonToPublicKey(_ jwkString: String) throws -> SecKey? {
|
||||||
guard let jsonData = jwkString.data(using: .utf8),
|
guard let jsonData = jwkString.data(using: .utf8),
|
||||||
let jwk = try? JSONDecoder().decode(ECPublicKey.self, from: jsonData),
|
let jwk = try? JSONDecoder().decode(ECPublicKey.self, from: jsonData),
|
||||||
@@ -32,12 +44,11 @@ class JWTUtil {
|
|||||||
return JWSHeader(headerData)
|
return JWSHeader(headerData)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func createJwt(header: JWTHeader, payload: JWTPayload, jwk: JWK) -> String? {
|
public static func createJwt(header: String, payload: String, jwk: String) -> String? {
|
||||||
guard let header = try? header.toJWSHeader(),
|
guard let header = headerStringToPayload(header),
|
||||||
let payload = try? payload.toPayload(),
|
let payload = payloadStringToPayload(payload),
|
||||||
let key = try? jwk.toPrivateSecKey()
|
let key = try? jsonToPrivateKey(jwk)
|
||||||
else {
|
else {
|
||||||
print("didn't have one")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,8 +64,8 @@ class JWTUtil {
|
|||||||
return jws.compactSerializedString
|
return jws.compactSerializedString
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func verifyJwt(token: String, jwk: JWK) -> JWTVerifyResponse? {
|
public static func verifyJwt(token: String, jwk: String) -> [String: Any]? {
|
||||||
guard let key = try? jwk.toPublicSecKey(),
|
guard let key = try? jsonToPublicKey(jwk),
|
||||||
let jws = try? JWS(compactSerialization: token),
|
let jws = try? JWS(compactSerialization: token),
|
||||||
let verifier = Verifier(verifyingAlgorithm: .ES256, key: key),
|
let verifier = Verifier(verifyingAlgorithm: .ES256, key: key),
|
||||||
let validation = try? jws.validate(using: verifier)
|
let validation = try? jws.validate(using: verifier)
|
||||||
@@ -63,24 +74,33 @@ class JWTUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let header = validation.header
|
let header = validation.header
|
||||||
let serializedHeader = JWTHeader(
|
|
||||||
alg: "ES256",
|
|
||||||
jku: Field(wrappedValue: header.jku?.absoluteString),
|
|
||||||
kid: Field(wrappedValue:header.kid),
|
|
||||||
typ: Field(wrappedValue: header.typ),
|
|
||||||
cty: Field(wrappedValue: header.cty),
|
|
||||||
crit: Field(wrappedValue: header.cty)
|
|
||||||
)
|
|
||||||
|
|
||||||
let payload = String(data: validation.payload.data(), encoding: .utf8)
|
let payload = String(data: validation.payload.data(), encoding: .utf8)
|
||||||
|
|
||||||
guard let payload = payload else {
|
guard let payload = payload else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return JWTVerifyResponse(
|
var protectedHeader: [String:Any] = [:]
|
||||||
protectedHeader: serializedHeader.toField(),
|
protectedHeader["alg"] = "ES256"
|
||||||
payload: payload.toField()
|
if header.jku != nil {
|
||||||
)
|
protectedHeader["jku"] = header.jku?.absoluteString
|
||||||
|
}
|
||||||
|
if header.kid != nil {
|
||||||
|
protectedHeader["kid"] = header.kid
|
||||||
|
}
|
||||||
|
if header.typ != nil {
|
||||||
|
protectedHeader["typ"] = header.typ
|
||||||
|
}
|
||||||
|
if header.cty != nil {
|
||||||
|
protectedHeader["cty"] = header.cty
|
||||||
|
}
|
||||||
|
if header.crit != nil {
|
||||||
|
protectedHeader["crit"] = header.crit
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
"payload": payload,
|
||||||
|
"protectedHeader": protectedHeader
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {requireNativeModule} from 'expo-modules-core'
|
import {requireNativeModule} from 'expo-modules-core'
|
||||||
import {Jwk, Jwt, Key} from '@atproto/jwk'
|
import {Jwk, Jwt} from '@atproto/jwk'
|
||||||
|
|
||||||
const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient')
|
const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient')
|
||||||
|
|
||||||
@@ -24,7 +24,9 @@ export const OauthClientReactNative = (NativeModule as null) || {
|
|||||||
*
|
*
|
||||||
* @throws if the algorithm is not supported ("ES256" must be supported)
|
* @throws if the algorithm is not supported ("ES256" must be supported)
|
||||||
*/
|
*/
|
||||||
async generateJwk(_algo: string): Promise<{publicKey: Key; privateKey: Key}> {
|
async generateJwk(
|
||||||
|
_algo: string,
|
||||||
|
): Promise<{publicKey: string; privateKey: string}> {
|
||||||
throw new Error(LINKING_ERROR)
|
throw new Error(LINKING_ERROR)
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -40,8 +42,8 @@ export const OauthClientReactNative = (NativeModule as null) || {
|
|||||||
_token: Jwt,
|
_token: Jwt,
|
||||||
_jwk: Jwk,
|
_jwk: Jwk,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
payload: string // this is a JSON response to make Swift a bit easier to work with
|
payload: string
|
||||||
protectedHeader: Record<string, unknown>
|
protectedHeader: string
|
||||||
}> {
|
}> {
|
||||||
throw new Error(LINKING_ERROR)
|
throw new Error(LINKING_ERROR)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export class ReactNativeKey extends Key {
|
|||||||
// Note: OauthClientReactNative.generatePrivateJwk should throw if it
|
// Note: OauthClientReactNative.generatePrivateJwk should throw if it
|
||||||
// doesn't support the algorithm.
|
// doesn't support the algorithm.
|
||||||
const res = await OauthClientReactNative.generateJwk(algo)
|
const res = await OauthClientReactNative.generateJwk(algo)
|
||||||
const jwk = res.privateKey
|
const jwk = JSON.parse(res.privateKey) as Record<string, unknown>
|
||||||
const use = jwk.use || 'sig'
|
const use = jwk.use || 'sig'
|
||||||
return new ReactNativeKey(jwkValidator.parse({...jwk, use, kid}))
|
return new ReactNativeKey(jwkValidator.parse({...jwk, use, kid}))
|
||||||
} catch {
|
} catch {
|
||||||
@@ -31,7 +31,11 @@ export class ReactNativeKey extends Key {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createJwt(header: JwtHeader, payload: JwtPayload): Promise<Jwt> {
|
async createJwt(header: JwtHeader, payload: JwtPayload): Promise<Jwt> {
|
||||||
return OauthClientReactNative.createJwt(header, payload, this.jwk)
|
return await OauthClientReactNative.createJwt(
|
||||||
|
JSON.stringify(header),
|
||||||
|
JSON.stringify(payload),
|
||||||
|
JSON.stringify(this.jwk),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyJwt<
|
async verifyJwt<
|
||||||
@@ -40,16 +44,11 @@ export class ReactNativeKey extends Key {
|
|||||||
>(token: Jwt, options?: VerifyOptions<C>): Promise<VerifyResult<P, C>> {
|
>(token: Jwt, options?: VerifyOptions<C>): Promise<VerifyResult<P, C>> {
|
||||||
const result = await OauthClientReactNative.verifyJwt(token, this.jwk)
|
const result = await OauthClientReactNative.verifyJwt(token, this.jwk)
|
||||||
|
|
||||||
// TODO see if we can make these `undefined` or maybe update zod to allow `nullable()`
|
|
||||||
let payloadParsed = JSON.parse(result.payload)
|
let payloadParsed = JSON.parse(result.payload)
|
||||||
payloadParsed = Object.fromEntries(
|
|
||||||
Object.entries(payloadParsed as object).filter(([_, v]) => v !== null),
|
|
||||||
)
|
|
||||||
const payload = jwtPayloadSchema.parse(payloadParsed)
|
const payload = jwtPayloadSchema.parse(payloadParsed)
|
||||||
|
|
||||||
// We don't need to validate this, because the native types ensure it is correct. But this is a TODO
|
let protectedHeaderParsed = JSON.parse(result.protectedHeader)
|
||||||
// for the same reason above
|
const protectedHeader = jwtPayloadSchema.parse(protectedHeaderParsed)
|
||||||
const protectedHeader = result.protectedHeader
|
|
||||||
|
|
||||||
if (options?.audience != null) {
|
if (options?.audience != null) {
|
||||||
const audience = Array.isArray(options.audience)
|
const audience = Array.isArray(options.audience)
|
||||||
|
|||||||
Reference in New Issue
Block a user