Skip to main content

GitHub ์•ฑ์— ๋Œ€ํ•œ JWT(JSON Web Token) ์ƒ์„ฑ

GitHub App์„(๋ฅผ) ์‚ฌ์šฉํ•˜์—ฌ ํŠน์ • REST API ์—”๋“œํฌ์ธํŠธ์— ์ธ์ฆํ•˜๋Š” JWT(JSON Web Token)๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ด…๋‹ˆ๋‹ค.

์ด ๋ฌธ์„œ์˜ ๋‚ด์šฉ

JWT(JSON Web Token) ์ •๋ณด

์•ฑ์œผ๋กœ ์ธ์ฆํ•˜๊ฑฐ๋‚˜ ์„ค์น˜ ์•ก์„ธ์Šค ํ† ํฐ์„ ์ƒ์„ฑํ•˜๋ ค๋ฉด JWT(JSON Web Token)๋ฅผ ์ƒ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. REST API ์—”๋“œํฌ์ธํŠธ๊ฐ€ JWT๋ฅผ ํ•„์š”ํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ์—”๋“œํฌ์ธํŠธ์— ๋Œ€ํ•œ ์„ค๋ช…์„œ์—๋Š” JWT๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์—”๋“œํฌ์ธํŠธ์— ์•ก์„ธ์Šคํ•ด์•ผ ํ•จ์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.

JWT๋Š” RS256 ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ช…๋˜์–ด์•ผ ํ•˜๋ฉฐ ๋‹ค์Œ ํด๋ ˆ์ž„์„ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํด๋ ˆ์ž„์˜๋ฏธ์„ธ๋ถ€ ์ •๋ณด
iat๋ฐœ๊ธ‰ ์‹œ๊ฐ„JWT๊ฐ€ ๋งŒ๋“ค์–ด์งˆ ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค. ํด๋ก ๋“œ๋ฆฌํ”„ํŠธ๋ฅผ ๋ฐฉ์ง€ํ•˜๋ ค๋ฉด ์ด๋ฅผ ๊ณผ๊ฑฐ 60์ดˆ๋กœ ์„ค์ •ํ•˜๊ณ  ์„œ๋ฒ„์˜ ๋‚ ์งœ์™€ ์‹œ๊ฐ„์„ ์ •ํ™•ํ•˜๊ฒŒ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค(์˜ˆ: ๋„คํŠธ์›Œํฌ ํƒ€์ž„ ํ”„๋กœํ† ์ฝœ ์‚ฌ์šฉ).
exp๋งŒ๋ฃŒ ์‹œ๊ฐ„JWT์˜ ๋งŒ๋ฃŒ ์‹œ๊ฐ„์ด๋ฉฐ, ๊ทธ ํ›„์—๋Š” ์„ค์น˜ ํ† ํฐ์„ ์š”์ฒญํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์‹œ๊ฐ„์€ ์•ž์œผ๋กœ 10๋ถ„ ์ด๋‚ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.
iss๋ฐœ๊ธ‰์žGitHub App์˜ ํด๋ผ์ด์–ธํŠธ ID ๋˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ID์ž…๋‹ˆ๋‹ค. ์ด ๊ฐ’์€ JWT์˜ ์„œ๋ช…์„ ํ™•์ธํ•˜๋Š” ๋ฐ ์ ํ•ฉํ•œ ๊ณต๊ฐœ ํ‚ค๋ฅผ ์ฐพ๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. GitHub App์˜ ์„ค์ • ํŽ˜์ด์ง€์—์„œ ์•ฑ์˜ ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ ID๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. GitHub App์˜ ์„ค์ • ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ GitHub ์•ฑ ๋“ฑ๋ก ์ˆ˜์ •์„(๋ฅผ) ์ฐธ์กฐํ•˜์„ธ์š”.
alg๋ฉ”์‹œ์ง€ ์ธ์ฆ ์ฝ”๋“œ ์•Œ๊ณ ๋ฆฌ์ฆ˜์ด๊ฒƒ์€ RS256 ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ•˜์—ฌ JWT๊ฐ€ ์„œ๋ช…๋˜์–ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— RS256์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

JWT๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด API ์š”์ฒญ์˜ Authorization ํ—ค๋”์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ์‹œ:

curl --request GET \
--url "https://api.github.com/app" \
--header "Accept: application/vnd.github+json" \
--header "Authorization: Bearer YOUR_JWT" \
--header "X-GitHub-Api-Version: 2022-11-28"

๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ Authorization: Bearer ๋˜๋Š” Authorization: token์„ ์‚ฌ์šฉํ•˜์—ฌ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ JWT(JSON ์›น ํ† ํฐ)๋ฅผ ์ „๋‹ฌํ•˜๋Š” ๊ฒฝ์šฐ Authorization: Bearer๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

JWT(JSON Web Token) ์ƒ์„ฑ

๋Œ€๋ถ€๋ถ„์˜ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์—๋Š” JWT๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ํŒจํ‚ค์ง€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ๊ฒฝ์šฐ์— GitHub App์˜ ํ”„๋ผ์ด๋น— ํ‚ค์™€ ID๊ฐ€ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ”„๋ผ์ด๋น— ํ‚ค ์ƒ์„ฑ์— ๊ด€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ GitHub ์•ฑ์— ๋Œ€ํ•œ ํ”„๋ผ์ด๋น— ํ‚ค ๊ด€๋ฆฌ์„(๋ฅผ) ์ฐธ์กฐํ•˜์„ธ์š”. GET /app REST API ์—”๋“œํฌ์ธํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์•ฑ์˜ ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ REST API ์„ค๋ช…์„œ์— ์žˆ๋Š” ์•ฑ์„ ์ฐธ์กฐํ•˜์„ธ์š”.

์ฐธ๊ณ  ํ•ญ๋ชฉ

JWT๋ฅผ ๋งŒ๋“œ๋Š” ๋Œ€์‹  GitHub์˜ Oktokit SDK๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์•ฑ์œผ๋กœ ์ธ์ฆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. SDK๋Š” JWT ์ƒ์„ฑ์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜๋ฉด JWT๋ฅผ ๋‹ค์‹œ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ REST API ๋ฐ JavaScript๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์Šคํฌ๋ฆฝํŒ…์„ ์ฐธ์กฐํ•˜์„ธ์š”.

์˜ˆ: ๋ฃจ๋น„๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ JWT ์ƒ์„ฑ

์ฐธ๊ณ  ํ•ญ๋ชฉ

์ด ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด jwt ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•˜๊ธฐ ์œ„ํ•ด gem install jwt๋ฅผ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ ์˜ˆ์ œ์—์„œ๋Š” YOUR_PATH_TO_PEM์„(๋ฅผ) ํ”„๋ผ์ด๋น— ํ‚ค๊ฐ€ ์ €์žฅ๋œ ํŒŒ์ผ ๊ฒฝ๋กœ๋กœ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค. YOUR_CLIENT_ID์„(๋ฅผ) ์•ฑ์˜ ID์œผ๋กœ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค. YOUR_PATH_TO_PEM ๋ฐ YOUR_CLIENT_ID ๊ฐ’์„ ํฐ๋”ฐ์˜ดํ‘œ๋กœ ๋ฌถ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

require 'openssl'
require 'jwt'  # https://rubygems.org/gems/jwt

# Private key contents
private_pem = File.read("YOUR_PATH_TO_PEM")
private_key = OpenSSL::PKey::RSA.new(private_pem)

# Generate the JWT
payload = {
  # issued at time, 60 seconds in the past to allow for clock drift
  iat: Time.now.to_i - 60,
  # JWT expiration time (10 minute maximum)
  exp: Time.now.to_i + (10 * 60),
  
# GitHub App's client ID
  iss: "YOUR_CLIENT_ID"

}

jwt = JWT.encode(payload, private_key, "RS256")
puts jwt

์˜ˆ: ํŒŒ์ด์ฌ์„ ์‚ฌ์šฉํ•˜์—ฌ JWT ์ƒ์„ฑ

์ฐธ๊ณ  ํ•ญ๋ชฉ

์ด ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด PyJWT ๋ฐ cryptography ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•˜๊ธฐ ์œ„ํ•ด pip install PyJWT cryptography๋ฅผ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Python
#!/usr/bin/env python3
import sys
import time

import jwt

# Get PEM file path
if len(sys.argv) > 1:
    pem = sys.argv[1]
else:
    pem = input("Enter path of private PEM file: ")

# Get the Client ID
if len(sys.argv) > 2:
    client_id = sys.argv[2]
else:
    client_id = input("Enter your Client ID: ")

# Open PEM
with open(pem, 'rb') as pem_file:
    signing_key = pem_file.read()

payload = {
    # Issued at time
    'iat': int(time.time()),
    # JWT expiration time (10 minutes maximum)
    'exp': int(time.time()) + 600,
    
    # GitHub App's client ID
    'iss': client_id

}

# Create JWT
encoded_jwt = jwt.encode(payload, signing_key, algorithm='RS256')

print(f"JWT: {encoded_jwt}")

์ด ์Šคํฌ๋ฆฝํŠธ๋Š” ํ”„๋ผ์ด๋น— ํ‚ค๊ฐ€ ์ €์žฅ๋œ ํŒŒ์ผ ๊ฒฝ๋กœ์™€ ์•ฑ์˜ ID๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ๋˜๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•  ๋•Œ ์ด๋Ÿฌํ•œ ๊ฐ’์„ ์ธ๋ผ์ธ ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ์ œ: Bash๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ JWT ์ƒ์„ฑ

์ฐธ๊ณ  ํ•ญ๋ชฉ

์ด ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•  ๋•Œ ํด๋ผ์ด์–ธํŠธ ID์™€ ํ”„๋ผ์ด๋น— ํ‚ค๊ฐ€ ์ €์žฅ๋˜๋Š” ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Bash
#!/usr/bin/env bash

set -o pipefail
client_id=$1 # Client ID as first argument

pem=$( cat $2 ) # file path of the private key as second argument

now=$(date +%s)
iat=$((${now} - 60)) # Issues 60 seconds in the past
exp=$((${now} + 600)) # Expires 10 minutes in the future

b64enc() { openssl base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n'; }

header_json='{
    "typ":"JWT",
    "alg":"RS256"
}'
# Header encode
header=$( echo -n "${header_json}" | b64enc )

payload_json="{
    \"iat\":${iat},
    \"exp\":${exp},
    \"iss\":\"${client_id}\"
}"
# Payload encode
payload=$( echo -n "${payload_json}" | b64enc )

# Signature
header_payload="${header}"."${payload}"
signature=$(
    openssl dgst -sha256 -sign <(echo -n "${pem}") \
    <(echo -n "${header_payload}") | b64enc
)

# Create JWT
JWT="${header_payload}"."${signature}"
printf '%s\n' "JWT: $JWT"

์˜ˆ์ œ: PowerShell์„ ์‚ฌ์šฉํ•˜์—ฌ JWT ์ƒ์„ฑ

๋‹ค์Œ ์˜ˆ์ œ์—์„œ๋Š” YOUR_PATH_TO_PEM์„(๋ฅผ) ํ”„๋ผ์ด๋น— ํ‚ค๊ฐ€ ์ €์žฅ๋œ ํŒŒ์ผ ๊ฒฝ๋กœ๋กœ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค. YOUR_CLIENT_ID์„(๋ฅผ) ์•ฑ์˜ ID์œผ๋กœ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค. YOUR_PATH_TO_PEM ๊ฐ’์„ ํฐ๋”ฐ์˜ดํ‘œ๋กœ ๋ฌถ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

PowerShell
#!/usr/bin/env pwsh

$client_id = YOUR_CLIENT_ID

$private_key_path = "YOUR_PATH_TO_PEM"

$header = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{
  alg = "RS256"
  typ = "JWT"
}))).TrimEnd('=').Replace('+', '-').Replace('/', '_');

$payload = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{
  iat = [System.DateTimeOffset]::UtcNow.AddSeconds(-10).ToUnixTimeSeconds()
  exp = [System.DateTimeOffset]::UtcNow.AddMinutes(10).ToUnixTimeSeconds()
   iss = $client_id 
}))).TrimEnd('=').Replace('+', '-').Replace('/', '_');

$rsa = [System.Security.Cryptography.RSA]::Create()
$rsa.ImportFromPem((Get-Content $private_key_path -Raw))

$signature = [Convert]::ToBase64String($rsa.SignData([System.Text.Encoding]::UTF8.GetBytes("$header.$payload"), [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)).TrimEnd('=').Replace('+', '-').Replace('/', '_')
$jwt = "$header.$payload.$signature"
Write-Host $jwt