To generate a JWT for a GitHub App, build a strict JSON claims object containing the app's identifier and short-lived timestamps, sign the encoded header and payload with HMAC-SHA-256 using a shared UTF-8 secret of at least 32 bytes, and concatenate the three base64url segments with periods. The JWT Generator on this page produces that compact HS256 string entirely in your browser, with the protected header pinned to {"alg":"HS256","typ":"JWT"} and unpadded RFC 4648 base64url encoding on every segment. The result is a valid development token suitable for testing the JWT creation step in your GitHub App authentication pipeline and for verifying the behavior of your JOSE library. It is not, however, a substitute for the RS256 private-key signing that GitHub's API actually expects from a production GitHub App — that signing step belongs to a maintained JOSE library and your registered PEM private key. Use this page to learn and validate the JWT structure, then sign real GitHub App tokens with the library your platform supports.

generate jwt for github app
generate jwt for github app

The role of a signed JWT in GitHub App authentication

GitHub Apps authenticate to the GitHub API through a two-step exchange. First, the app sends a short-lived JWT to the /app authentication endpoint; GitHub verifies the signature, checks the issuer claim against the registered app ID, and confirms the token has not expired. If everything matches, GitHub returns an installation access token tied to a specific repository or organization installation. That installation token is what your scripts and CI jobs use for day-to-day API calls.

The JWT itself carries only a handful of claims. The issuer (iss) is your GitHub App's numeric ID. The issued-at (iat) timestamp is the moment the JWT was created, expressed in Unix seconds. The expiration (exp) caps the token's lifetime — GitHub currently caps it at ten minutes for apps and rejects anything longer. GitHub also accepts an optional not-before (nbf) timestamp and rejects tokens that are too far in the future. Anything else — installation IDs, repository names, user scopes — does not belong in the JWT itself and is supplied alongside the installation token instead.

This is why the JWT format matters even though the token is never used for routine requests. A malformed JSON claims object, a wrong algorithm, or an expired timestamp causes GitHub to reject the entire authentication, and your integration will see 401 responses until the JWT is built correctly. Building one locally lets you isolate whether the failure is in your construction code or in the network call.

HS256 versus RS256: which algorithm fits which step

GitHub's API expects GitHub App JWTs to be signed with RS256, an asymmetric algorithm that uses an RSA private key on the app side and the public key GitHub already stores for the app on its side. HS256 is a symmetric algorithm that uses the same shared secret for signing and verification, so it cannot be used to authenticate against github.com on its own. The JWT Generator on this page emits HS256 tokens and explicitly fixes the protected header to {"alg":"HS256","typ":"JWT"}. That makes the page useful for the structural step — building claims, encoding segments, and verifying the resulting string — but it does not produce a token the GitHub API will accept from a real GitHub App.

PropertyHS256 (this page)RS256 (production GitHub Apps)
Key typeShared symmetric secret, 32+ UTF-8 bytesRSA private key (PEM), 2048+ bits
Verification keySame secret on both sidesPublic key already registered with GitHub
JWA referenceRFC 7518 HMAC with SHA-256RFC 7518 RSASSA-PKCS1-v1_5 with SHA-256
Acceptable to github.comNoYes
Use on this pageLearning JWT structure and testing verifiersProduction GitHub App authentication

The fixed algorithm matters for safety. Allowing user input to choose the algorithm is how "alg":"none" or algorithm-confusion attacks get introduced. By pinning HS256, the page prevents a casual paste from accidentally generating an unsecured or asymmetric-looking token.

Generate a GitHub App JWT locally with the JWT Generator

  1. Open the JWT Generator and paste a strict JSON claims object into the claims field. The object must be syntactically valid JSON; arrays, null, comments, trailing commas, undefined, NaN, Infinity, and JavaScript expressions are rejected. A minimal GitHub App-style claims object looks like {"iss":"123456","iat":1719436800,"exp":1719437400} — substitute your real numeric app ID and current Unix-second timestamps.
  2. Enter a secret in the secret field, or click the random button to generate one. The random button produces 32 cryptographically random bytes displayed as 64 hexadecimal characters; those characters are used as the UTF-8 HMAC secret. The secret must contain at least 32 UTF-8 bytes; character count and byte count can differ for non-ASCII text. For a GitHub App flow this is a development-only placeholder — production secrets are your PEM private key.
  3. Click the generate button. The tool compiles the fixed header {"alg":"HS256","typ":"JWT"}, encodes your claims object, signs the encoded header plus a period plus the encoded payload with HMAC-SHA-256, and emits three compact segments separated by periods.
  4. Inspect the segments. The first segment is the base64url-encoded header, always 36 characters for the fixed HS256/JWT header. The second segment is the base64url-encoded claims object, whose length depends on your input. The third segment is the base64url-encoded 32-byte signature, which encodes to 43 unpadded characters.
  5. Copy the resulting token. The copy action places the compact string on your clipboard. Treat clipboard contents as bearer credentials — they may be retained by clipboard history, screenshots, browser extensions, logs, or chat tools.
  6. Verify the copied token with a maintained JOSE library before treating it as production-shaped output. The verification call must specify the algorithm explicitly, the expected issuer, and any audience or lifetime policy your application enforces. Only a verified token is trustworthy.

Anatomy of the three compact JWT segments

Every JWT, regardless of the algorithm, is three base64url strings separated by periods. The structure is defined in RFC 7519 and the signing rules in RFC 7515. For the fixed HS256 header on this page, the segments always follow the same shape:

SegmentContentEncodingLength for HS256 here
Header{"alg":"HS256","typ":"JWT"}UTF-8 then RFC 4648 base64url, no padding36 characters
PayloadYour strict JSON claims objectUTF-8 then RFC 4648 base64url, no paddingDepends on claim count and length
SignatureHMAC-SHA-256 of header.payload, using the 32+ byte UTF-8 secretRFC 4648 base64url of the raw 32-byte MAC, no padding43 characters

The signing input is exactly the byte sequence <encoded-header>.<encoded-payload> — a single period between the two segments, no surrounding whitespace, no trailing newline. Web Crypto imports the secret as a raw HMAC key with SHA-256 and signs that exact byte sequence. Any change to a character in the header, the payload, or the period placement will change the third segment after verification, which is the integrity property JWTs are designed to provide.

If you want to inspect what you just generated, paste the token into the JWT Decoder to base64url-decode the header and payload locally. Decoding shows the claims you actually emitted — useful for catching typos in iss or transposed digits in iat before you ship the token to a verifier.

Verify the copied token with a maintained JOSE library

A syntactically valid HS256 token is not necessarily a safe or acceptable one. Verification is the step that confirms the signature, the issuer, the audience, and the lifetime against your application's trust policy. The generator does not perform this step — it only emits the compact string.

A minimal verification call uses an explicit algorithm allowlist, the expected issuer claim, the shared secret, and a tolerance for clock skew on iat, nbf, and exp. NumericDate values are seconds from the Unix epoch, not JavaScript milliseconds; if your verifier uses millisecond timestamps, the JWT will appear to have expired decades ago. Allow the verifier to reject unknown header parameters and unexpected algorithms even if the signature checks out — this is the defense that prevents algorithm-confusion attacks if a token from a different system is ever presented to your service. If you want a no-code walkthrough of what your verifier is reading, the guide on how to decode a JWT access token in your browser shows the same segments from the verification side.

For the GitHub App context specifically, do not feed an HS256 token generated here to the GitHub API — it will be rejected because the algorithm does not match what GitHub stores for your app. Use this page to validate the JWT construction pipeline, then switch to your library's RS256 mode with your registered PEM private key when you actually call GitHub.

Safety considerations before pasting secrets or claims

HS256 signs the compact data but does not hide it. Anyone who receives the token can base64url-decode the header and the payload without the secret. Never put passwords, private keys, personal records, or confidential application data into a JWT merely because it is signed. The signature detects modification only when a verifier uses the correct secret and validates the result.

Generated random secrets are 64 hexadecimal characters derived from 32 cryptographically random bytes, but they are displayed in the browser and may be captured by screenshots, browser extensions, or clipboard tools. Treat them as visible development material, not automatically provisioned production secrets. If a real secret or active token is pasted into any environment outside its intended boundary, rotate or revoke it instead of assuming the local implementation made disclosure harmless.

The claims input must be a strict JSON object — this is enforced, not advisory. Comments, trailing commas, and JavaScript expressions are not allowed because they would let a casually pasted string drift away from what your verifier expects. When in doubt, validate the claims object with the JSON Validator before pasting it into the generator so the error location is pinned to a line and column.