{"id":19700513,"url":"https://github.com/vapor/jwt-kit","last_synced_at":"2025-04-05T23:12:42.243Z","repository":{"id":39543552,"uuid":"226194576","full_name":"vapor/jwt-kit","owner":"vapor","description":"🔑 JSON Web Token (JWT) signing and verification (HMAC, ECDSA, EdDSA, RSA, PSS) with support for JWS and JWK","archived":false,"fork":false,"pushed_at":"2024-04-24T08:03:39.000Z","size":4445,"stargazers_count":161,"open_issues_count":2,"forks_count":47,"subscribers_count":10,"default_branch":"main","last_synced_at":"2024-05-01T11:26:40.172Z","etag":null,"topics":["jwt","server-side-swift","swift"],"latest_commit_sha":null,"homepage":"https://api.vapor.codes/jwtkit/documentation/jwtkit/","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vapor.png","metadata":{"funding":{"github":["vapor"],"open_collective":"vapor"},"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-12-05T21:46:30.000Z","updated_at":"2024-06-10T09:40:13.825Z","dependencies_parsed_at":"2023-10-23T15:34:03.136Z","dependency_job_id":"08fef8fb-b2fb-44b9-9de1-cf219d7d88da","html_url":"https://github.com/vapor/jwt-kit","commit_stats":{"total_commits":79,"total_committers":20,"mean_commits":3.95,"dds":0.810126582278481,"last_synced_commit":"f1a1bae0fbcfe451cde60556e0b0544a1b129a2f"},"previous_names":[],"tags_count":51,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vapor%2Fjwt-kit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vapor%2Fjwt-kit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vapor%2Fjwt-kit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vapor%2Fjwt-kit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vapor","download_url":"https://codeload.github.com/vapor/jwt-kit/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247411239,"owners_count":20934653,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["jwt","server-side-swift","swift"],"created_at":"2024-11-11T21:06:16.947Z","updated_at":"2025-04-05T23:12:42.224Z","avatar_url":"https://github.com/vapor.png","language":"Swift","funding_links":["https://github.com/sponsors/vapor","https://opencollective.com/vapor"],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n\u003cpicture\u003e\n  \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://github.com/vapor/jwt-kit/assets/1130717/06939767-8779-42ea-9bb6-9d3e7a07d20c\"\u003e\n  \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"https://github.com/vapor/jwt-kit/assets/1130717/bdc5befe-01c4-4e50-a203-c6ef71e16394\"\u003e\n  \u003cimg src=\"https://github.com/vapor/jwt-kit/assets/1130717/bdc5befe-01c4-4e50-a203-c6ef71e16394\" height=\"96\" alt=\"JWTKit\"\u003e\n\u003c/picture\u003e \n\u003cbr\u003e\n\u003cbr\u003e\n\u003ca href=\"https://docs.vapor.codes/security/jwt\"\u003e\u003cimg src=\"https://design.vapor.codes/images/readthedocs.svg\" alt=\"Documentation\"\u003e\u003c/a\u003e\n\u003ca href=\"https://discord.gg/vapor\"\u003e\u003cimg src=\"https://design.vapor.codes/images/discordchat.svg\" alt=\"Team Chat\"\u003e\u003c/a\u003e\n\u003ca href=\"LICENSE\"\u003e\u003cimg src=\"https://design.vapor.codes/images/mitlicense.svg\" alt=\"MIT License\"\u003e\u003c/a\u003e\n\u003ca href=\"https://github.com/vapor/jwt-kit/actions/workflows/test.yml\"\u003e\u003cimg src=\"https://img.shields.io/github/actions/workflow/status/vapor/jwt-kit/test.yml?event=push\u0026style=plastic\u0026logo=github\u0026label=tests\u0026logoColor=%23ccc\" alt=\"Continuous Integration\"\u003e\u003c/a\u003e\n\u003ca href=\"https://codecov.io/github/vapor/jwt-kit\"\u003e\u003cimg src=\"https://img.shields.io/codecov/c/github/vapor/jwt-kit?style=plastic\u0026logo=codecov\u0026label=codecov\"\u003e\u003c/a\u003e\n\u003ca href=\"https://swift.org\"\u003e\u003cimg src=\"https://design.vapor.codes/images/swift60up.svg\" alt=\"Swift 6.0+\"\u003e\u003c/a\u003e\n\u003ca href=\"https://www.swift.org/sswg/incubation-process.html\"\u003e\u003cimg src=\"https://design.vapor.codes/images/sswg-graduated.svg\" alt=\"SSWG Incubation Level: Graduated\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\u003cbr\u003e\n\n🔑 JSON Web Token signing and verification (HMAC, RSA, PSS, ECDSA, EdDSA) using SwiftCrypto.\n\n### Supported Platforms\n\nJWTKit supports all platforms supported by Swift 6 and later.\n\n### Installation\n\nUse the SPM string to easily include the dependendency in your `Package.swift` file\n\n```swift\n.package(url: \"https://github.com/vapor/jwt-kit.git\", from: \"5.0.0\")\n```\n\nand add it to your target's dependencies:\n\n```swift\n.product(name: \"JWTKit\", package: \"jwt-kit\")\n```\n\n## Overview\n\nJWTKit provides APIs for signing and verifying JSON Web Tokens, as specified by [RFC 7519](https://www.rfc-editor.org/rfc/rfc7519.html). The following features are supported:\n\n- Signing and Verification with Custom Headers\n- Customisable Parsing and Serialization\n- JSON Web Keys (`JWK`, `JWKS`)\n\nThe following algorithms, as defined in [RFC 7518 § 3](https://www.rfc-editor.org/rfc/rfc7518.html#section-3) and [RFC 8037 § 3](https://www.rfc-editor.org/rfc/rfc8037.html#section-3), are supported for both signing and verification:\n\n| JWS | Algorithm | Description |\n| :-------------: | :-------------: | :----- |\n| HS256 | HMAC256 | HMAC with SHA-256 |\n| HS384 | HMAC384 | HMAC with SHA-384 |\n| HS512 | HMAC512 | HMAC with SHA-512 |\n| RS256 | RSA256 | RSASSA-PKCS1-v1_5 with SHA-256 |\n| RS384 | RSA384 | RSASSA-PKCS1-v1_5 with SHA-384 |\n| RS512 | RSA512 | RSASSA-PKCS1-v1_5 with SHA-512 |\n| PS256 | RSA256PSS | RSASSA-PSS with SHA-256 |\n| PS384 | RSA384PSS | RSASSA-PSS with SHA-384 |\n| PS512 | RSA512PSS | RSASSA-PSS with SHA-512 |\n| ES256 | ECDSA256 | ECDSA with curve P-256 and SHA-256 |\n| ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 |\n| ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 |\n| EdDSA | EdDSA | EdDSA with Ed25519 |\n| none | None | No digital signature or MAC |\n\n## Vapor\n\nThe [vapor/jwt](https://github.com/vapor/jwt) package provides first-class integration with Vapor and is recommended for all Vapor projects which want to use JWTKit.\n\n## Getting Started\n\nA `JWTKeyCollection` object is used to load signing keys and keysets, and to sign and verify tokens: \n\n```swift\nimport JWTKit\n\n// Signs and verifies JWTs\nlet keys = JWTKeyCollection()\n```\n\nTo add a signing key to the collection, use the `add` method for the respective algorithm:\n\n```swift\n// Registers an HS256 (HMAC-SHA-256) signer.\nawait keys.add(hmac: \"secret\", digestAlgorithm: .sha256)\n```\n\nThis example uses the _very_ secure key `\"secret\"`.\n\nYou can also add an optional key identifier (`kid`) to the key:\n\n```swift\n// Registers an HS256 (HMAC-SHA-256) signer with a key identifier.\nawait keys.add(hmac: \"secret\", digestAlgorithm: .sha256, kid: \"my-key\")\n```\n\nThis is useful when you have multiple keys and need to select the correct one for verification. Based on the `kid` defined in the JWT header, the correct key will be selected for verification.\nIf you don't provide a `kid`, the key will be added to the collection as default.\n\n\u003e [!NOTE]\n\u003e If multiple keys are added all without a `kid`, only the last one will be stored and the previous ones will be overwritten, which means if you want to store multiple keys you need to provide a `kid` for each one.\n\nTo ensure thread-safety, `JWTKeyCollection` is an `actor`. This means that all of its methods are `async` and must be `await`ed.\n\n### Signing\n\nWe can _generate_ JWTs, also known as signing. To demonstrate this, let's create a payload. Each property of the payload type corresponds to a claim in the token. JWTKit provides predefined types for all of the claims specified by RFC 7519, as well as some convenience types for working with custom claims. For the example token, the payload looks like this:\n\n```swift\nstruct ExamplePayload: JWTPayload {\n    var sub: SubjectClaim\n    var exp: ExpirationClaim\n    var admin: BoolClaim\n\n    func verify(using key: some JWTAlgorithm) throws {\n        try self.exp.verifyNotExpired()\n    }\n}\n\n// Create a new instance of our JWTPayload\nlet payload = ExamplePayload(\n    subject: \"vapor\",\n    expiration: .init(value: .distantFuture),\n    isAdmin: true\n)\n```\n\nThen, pass the payload to `JWTKeyCollection.sign`. \n\n```swift\n// Sign the payload, returning the JWT as String\nlet jwt = try await keys.sign(payload, kid: \"my-key\")\nprint(jwt)\n```\n\nHere we've added a custom header to the JWT. Any key-value pair can be added to the header. In this case the `kid` will be used to look up the correct key for verification from the `JWTKeyCollection`.\n\nYou should see a JWT printed. This can be fed back into the `verify` method to access the payload.\n\n### Verifying\n\nLet's try to verify the following example JWT:\n\n```swift\nlet exampleJWT = \"\"\"\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo\n\"\"\"\n```\n\nYou can inspect the contents of this token by visiting [jwt.io](https://jwt.io) and pasting the token in the debugger. Set the key in the \"Verify Signature\" section to `secret`. \n\nTo verify a token, the format of the payload must be known. In this case, we know that the payload is of type `ExamplePayload`. Using this payload, the `JWTKeyCollection` object can process and verify the example JWT, returning its payload on success:\n\n```swift\n// Parse the JWT, verify its signature and decode its content\nlet payload = try await keys.verify(exampleJWT, as: ExamplePayload.self)\nprint(payload)\n```\n\nIf all works correctly, this code will print something like this:\n\n```swift\nTestPayload(\n    sub: SubjectClaim(value: \"vapor\"),\n    exp: ExpirationClaim(value: 4001-01-01 00:00:00 +0000),\n    admin: BoolClaim(value: true)\n)\n```\n\n\u003e [!NOTE]\n\u003e The `admin` property of the example payload did not have to use the `BoolClaim` type; a simple `Bool` would have worked as well. The `BoolClaim` type is provided by JWTKit for convenience in working with the many JWT implementations which encode boolean values as JSON strings (e.g. `\"true\"` and `\"false\"`) rather than using JSON's `true` and `false` keywords.   \n\n## JWK\n\nA JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data structure that represents a cryptographic key, defined in [RFC7517](https://www.rfc-editor.org/rfc/rfc7517.html). These are commonly used to supply clients with keys for verifying JWTs. For example, Apple hosts their _Sign in with Apple_ JWKS at the URL `https://appleid.apple.com/auth/keys`.\n\nYou can add this JSON Web Key Set (JWKS) to your `JWTSigners`: \n\n```swift\n#if !canImport(Darwin)\n    import FoundationEssentials\n#else\n    import Foundation\n#endif\nimport JWTKit\n\nlet rsaModulus = \"...\"\n\nlet json = \"\"\"\n{\n    \"keys\": [\n        {\"kty\": \"RSA\", \"alg\": \"RS256\", \"kid\": \"a\", \"n\": \"\\(rsaModulus)\", \"e\": \"AQAB\"},\n        {\"kty\": \"RSA\", \"alg\": \"RS512\", \"kid\": \"b\", \"n\": \"\\(rsaModulus)\", \"e\": \"AQAB\"},\n    ]\n}\n\"\"\"\n\n// Create key collection and add JWKS\nlet keys = try await JWTKeyCollection().add(jwksJSON: json)\n```\n\nYou can now pass JWTs from Apple to the `verify` method. The key identifier (`kid`) in the JWT header will be used to automatically select the correct key for verification. A JWKS may contain any of the key types supported by JWTKit.  \n\n## HMAC\n\nHMAC is the simplest JWT signing algorithm. It uses a single key that can both sign and verify tokens. The key can be any length.\n\nTo add an HMAC key to the key collection, use the `addHS256`, `addHS384`, or `addHS512` methods:\n\n```swift\n// Add HMAC with SHA-256 signer.\nawait keys.add(hmac: \"secret\", digestAlgorithm: .sha256)\n```\n\n\u003e [!IMPORTANT]  \n\u003e Cryptography is a complex topic, and the decision of algorithm can directly impact the integrity, security, and privacy of your data. This README does not attempt to offer a meaningful discussion of these concerns; the package authors recommend doing your own research before making a final decision.\n\n## ECDSA\n\nECDSA is a modern asymmetric algorithm based on elliptic curve cryptography.\nIt uses a public key to verify tokens and a private key to sign them.\n\nYou can load ECDSA keys using PEM files: \n\n```swift\nlet ecdsaPublicKey = \"-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----\"\n\n// Initialize an ECDSA key with public pem.\nlet key = try ES256PublicKey(pem: ecdsaPublicKey)\n```\n\nOnce you have an ECDSA key, you can add to the key collection using the following methods:\n\n- `addES256`: ECDSA with SHA-256\n- `addES384`: ECDSA with SHA-384\n- `addES512`: ECDSA with SHA-512\n\n```swift\n// Add ECDSA with SHA-256 algorithm\nawait keys.add(ecdsa: key)\n```\n\n## EdDSA\n\nEdDSA is a modern algorithm that is considered to be more secure than RSA and ECDSA. It is based on the Edwards-curve Digital Signature Algorithm. The only currently supported curve by JWTKit is Ed25519.\n\nYou can create an EdDSA key using its coordinates:\n\n```swift\n// Initialize an EdDSA key with public PEM\nlet publicKey = try EdDSA.PublicKey(x: \"...\", curve: .ed25519)\n\n// Initialize an EdDSA key with private PEM\nlet privateKey = try EdDSA.PrivateKey(x: \"...\", d: \"...\", curve: .ed25519)\n\n// Add public key to the key collection\nawait keys.add(eddsa: publicKey)\n\n// Add private key to the key collection\nawait keys.add(eddsa: privateKey)\n```\n\n## RSA\n\nRSA is an asymmetric algorithm. It uses a public key to verify tokens and a private key to sign them.\n\n\u003e [!WARNING]\\\n\u003e RSA is no longer recommended for new applications. If possible, use EdDSA or ECDSA instead. [Infosec Insights' June 2020 blog post \"ECDSA vs RSA: Everything You Need to Know\"](https://sectigostore.com/blog/ecdsa-vs-rsa-everything-you-need-to-know/) provides a detailed discussion on the differences between the two.\n\n\nTo create an RSA signer, first initialize an `RSAKey`. This can be done by passing in the components:\n\n```swift\n// Initialize an RSA key with components.\nlet key = try Insecure.RSA.PrivateKey(\n    modulus: \"...\",\n    exponent: \"...\",\n    privateExponent: \"...\"\n)\n```\n\nThe same initializer can be used for public keys without the `privateExponent` parameter.\n\nYou can also choose to load a PEM file:\n\n```swift\nlet rsaPublicKey = \"-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----\"\n\n// Initialize an RSA key with public PEM\nlet key = try Insecure.RSA.PublicKey(pem: rsaPublicKey)\n```\n\nUse `Insecure.RSA.PrivateKey(pem:)` for loading private RSA pem keys and `Insecure.RSA.PublicKey(certificatePEM:)` for loading X.509 certificates.\nOnce you have an RSA key, you can add to the key collection using the dedicated methods depending on the digest and the padding:\n\n```swift\n// Add RSA with SHA-256 algorithm \nawait keys.add(rsa: key, digestAlgorithm: .sha256)\n\n// Add RSA with SHA-512 and PSS padding algorithm\nawait keys.add(pss: key, digestAlgorithm: .sha512)\n```\n\n## Claims\n\nJWTKit includes several helpers for implementing the \"standard\" JWT claims defined by [RFC § 4.1](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1): \n\n|Claim|Type|Verify Method|\n|---|---|---|\n|`aud`|`AudienceClaim`|`verifyIntendedAudience(includes:)`|\n|`exp`|`ExpirationClaim`|`verifyNotExpired(currentDate:)`|\n|`jti`|`IDClaim`|n/a|\n|`iat`|`IssuedAtClaim`|n/a|\n|`iss`|`IssuerClaim`|n/a|\n|`nbf`|`NotBeforeClaim`|`verifyNotBefore(currentDate:)`|\n|`sub`|`SubjectClaim`|n/a|\n\nWhenever possible, all of a payload's claims should be verified in the `verify(using:)` method; those which do not have verification methods of their own may be verified manually.\n\nAdditional helpers are provided for common types of claims not defined by the RFC:\n\n- `BoolClaim`: May be used for any claim whose value is a boolean flag. Will recognize both boolean JSON values and the strings `\"true\"` and `\"false\"`.\n- `GoogleHostedDomainClaim`: For use with the `GoogleIdentityToken` vendor token type.\n- `JWTMultiValueClaim`: A protocol for claims, such as `AudienceClaim` which can optionally be encoded as an array with multiple values.\n- `JWTUnixEpochClaim`: A protocol for claims, such as `ExpirationClaim` and `IssuedAtClaim`, whose value is a count of seconds since the UNIX epoch (midnight of January 1, 1970).\n- `LocaleClaim`: A claim whose value is a [BCP 47](https://www.rfc-editor.org/info/bcp47) language tag. Also used by `GoogleIdentityToken`.\n\n## Custom Parsing and Serialization\n\nThe `JWTParser` and `JWTSerializer` protocols allow you to define custom parsing and serialization for your payload types. This is useful when you need to work with a non-standard JWT format.\n\nFor example you might need to set the `b64` header to false, which does not base64 encode the payload. You can create your own `JWTParser` and `JWTSerializer` to handle this.\n\n```swift\nstruct CustomSerializer: JWTSerializer {\n    // Here you can set a custom encoder or just leave this as default\n    var jsonEncoder: JWTJSONEncoder = .defaultForJWT\n\n    // This method should return the payload in the way you want/need it\n    func serialize(_ payload: some JWTPayload, header: JWTHeader) throws -\u003e Data {\n        // Check if the b64 header is set. If it is, base64URL encode the payload, don't otherwise\n        if header.b64?.asBool == true {\n            try Data(jsonEncoder.encode(payload).base64URLEncodedBytes())\n        } else {\n            try jsonEncoder.encode(payload)\n        }\n    }\n}\n\nstruct CustomParser: JWTParser {\n    // Here you can set a custom decoder or just leave this as default\n    var jsonDecoder: JWTJSONDecoder = .defaultForJWT\n\n    // This method parses the token into a tuple containing the various token's elements\n    func parse\u003cPayload\u003e(_ token: some DataProtocol, as: Payload.Type) throws -\u003e (header: JWTHeader, payload: Payload, signature: Data) where Payload: JWTPayload {\n        // A helper method is provided to split the token correctly\n        let (encodedHeader, encodedPayload, encodedSignature) = try getTokenParts(token)\n\n        // The header is usually always encoded the same way\n        let header = try jsonDecoder.decode(JWTHeader.self, from: .init(encodedHeader.base64URLDecodedBytes()))\n\n        // If the b64 header field is non present or true, base64URL decode the payload, don't otherwise\n        let payload = if header.b64?.asBool ?? true {\n            try jsonDecoder.decode(Payload.self, from: .init(encodedPayload.base64URLDecodedBytes()))\n        } else {\n            try jsonDecoder.decode(Payload.self, from: .init(encodedPayload))\n        }\n\n        // The signature is usually also always encoded the same way\n        let signature = Data(encodedSignature.base64URLDecodedBytes())\n\n        return (header: header, payload: payload, signature: signature)\n    }\n}\n```\nAnd then use them like this:\n\n```swift\nlet keyCollection = await JWTKeyCollection().add(\n    hmac: \"secret\", \n    digestAlgorithm: .sha256,\n    parser: CustomParser(), \n    serializer: CustomSerializer()\n)\n\nlet payload = TestPayload(sub: \"vapor\", name: \"Foo\", admin: false, exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)))\n\nlet token = try await keyCollection.sign(payload, header: [\"b64\": true])\n```\n\n## Custom JSON Encoder and Decoder\n\nIf you don't need to specify custom parsing and serializing but you do need to use a custom JSON Encoder or Decoder, you can use the the `DefaultJWTParser` and `DefaultJWTSerializer` types to create a `JWTKeyCollection` with a custom JSON Encoder and Decoder.\n\n```swift\nlet encoder = JSONEncoder()\nencoder.dateEncodingStrategy = .iso8601\nlet decoder = JSONDecoder() \ndecoder.dateDecodingStrategy = .iso8601\n\nlet parser = DefaultJWTParser(jsonDecoder: decoder)\nlet serializer = DefaultJWTSerializer(jsonEncoder: encoder)\n\nlet keyCollection = await JWTKeyCollection().add(\n    hmac: \"secret\",\n    digestAlgorithm: .sha256,\n    parser: parser, \n    serializer: serializer\n)\n```\n\n## Installation\n\nRun the following commands on your package using SwiftPM, replacing `MyTarget` with the name of your target:\n\n```swift\ncd /path/to/project/root/directory\nswift package add-dependency https://github.com/vapor/jwt-kit.git --from 5.0.0\nswift package add-target-dependency JWTKit MyTarget\n```\n\nOr manually add the following to your `Package.swift` file:\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/vapor/jwt-kit.git\", from: \"5.0.0\")\n],\ntargets: [\n  .target(\n    name: \"MyTarget\",\n    dependencies: [\n        .target(name: \"JWTKit\"),\n    ]),\n]\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvapor%2Fjwt-kit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvapor%2Fjwt-kit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvapor%2Fjwt-kit/lists"}