Base64とBase64URLでエンコード結果が変わる3か所
公開日: 2026-06-28
base64.b64decode() にJWTのセグメントを渡すと、Pythonでは binascii.Error: Invalid base64-encoded string が返ってくることがあります。- や _ が混ざっているか、末尾の = がないことが原因ですが、「+ を - に置き換えるだけ」という直し方では足りない場合があります。Base64とBase64URLで出力が変わる場所は3か所あり、それぞれ別の対処が必要です。
1. 文字置換:+と/が-と_になる
Base64のアルファベット64文字のうち、62番目と63番目の文字だけが変わります。
インデックス Base64 Base64URL 0〜61 A–Z, a–z, 0–9 同じ 62+
-
63
/
_
エンコード対象のバイト列に、この2文字に対応する6ビットパターン(111110 または 111111)が含まれているときだけ出力が変わります。含まれなければBase64とBase64URLの出力は完全に同じです。
import base64
data = b'\xfb\xef\xbe' # バイナリ: 11111011 11101111 10111110
print(base64.b64encode(data)) # b'++++'
print(base64.urlsafe_b64encode(data)) # b'----'
\xfb\xef\xbe の3バイトは6ビット単位で切り出すとすべて 111110(インデックス62)になるため、Base64では ++++、Base64URLでは ---- になります。変換は対称なので、デコード側では逆に -→+、_→/ と置き換えてから標準のデコード関数に渡せます。
2. パディング:=ありと=なし
Base64は3バイトを4文字に変換します。入力が3の倍数バイトでない場合、出力を4の倍数文字にするため末尾に = を1つか2つ付けます。
入力 1バイト → 出力 2文字 + "==" (パディング2個)
入力 2バイト → 出力 3文字 + "=" (パディング1個)
入力 3バイト → 出力 4文字 (パディングなし)
RFC 4648のSection 5(Base64url)は、受け取り側がデータの全長を事前に知っている場合に限り、パディングを省略可能と定めています。JWTはこの条件を満たすと判断し、3つのセグメント(ヘッダー・ペイロード・署名)すべてで = を省略します。
JWTのセグメントは「Base64URLかつパディングなし」です。Pythonの base64.urlsafe_b64decode() はパディングを要求するため、そのまま渡すとエラーになります。
import base64, json
segment = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
# パディングを補ってからデコードする
padding = (4 - len(segment) % 4) % 4
decoded = base64.urlsafe_b64decode(segment + '=' * padding)
print(json.loads(decoded)) # {'alg': 'HS256', 'typ': 'JWT'}
パディング補完の公式 (4 - len(s) % 4) % 4 は、len % 4 == 0 のときに4を足さないための二重mod演算です。len % 4 が0なら (4 - 0) % 4 = 0、2なら (4 - 2) % 4 = 2、3なら (4 - 3) % 4 = 1 と、必要なパディング数が得られます。
3. URLへの埋め込み:=が%3Dになる
標準Base64の出力をURLのクエリパラメータに埋め込むと、= はパーセントエンコードされ %3D になります。
# 標準Base64のクエリ文字列埋め込み(そのまま渡した場合)
?token=abc+def/ghi==
# URLエンコード後(URLとして正しいが冗長)
?token=abc%2Bdef%2Fghi%3D%3D
# Base64URL(URLセーフ、パディングなし)
?token=abc-def_ghi
+ はクエリ文字列内でスペースとして解釈されるパーサーが存在し、= はキーと値の区切りとして処理されることがあります。3番目の形式が最もクリーンです。これがBase64URLが生まれた理由です。
どの形式が使われているか
用途 形式 パディング JWT(ヘッダー・ペイロード・署名) Base64URL なし OAuth 2.0 PKCE(code_challenge) Base64URL なし Data URI(data:image/png;base64,...)
標準Base64
あり
MIME(メール添付)
標準Base64
あり
URLクエリパラメータに直接埋め込む場合
Base64URL
なし推奨
ファイル名として使う場合
Base64URL
なし推奨
デコード時に「どの形式で渡ってきたのか」を先に確認します。- や _ が含まれていればBase64URL、+ や / が含まれていれば標準Base64です。末尾の = の有無はパディング設定によるため、形式の判定材料にはなりません。
デコードの手順まとめ
形式が混在している可能性がある場合は、以下の順序で変換します。
import base64
def decode_base64_any(s: str) -> bytes:
# 1. Base64URL → Base64 の文字置換
s = s.replace('-', '+').replace('_', '/')
# 2. パディング補完
s += '=' * ((4 - len(s) % 4) % 4)
# 3. デコード
return base64.b64decode(s)
JavaScriptでは atob() が標準Base64しか受け付けないため、Base64URLを渡す前に同じ変換が必要です。
function decodeBase64Any(s) {
s = s.replace(/-/g, '+').replace(/_/g, '/');
while (s.length % 4 !== 0) s += '=';
return atob(s);
}
Node.js 16以降は 'base64url' エンコーディングが追加されたため、Buffer.from(s, 'base64url') でパディングなし・文字置換済みのBase64URLをそのままデコードできます。ただしこれはNode.js専用の仕様なので、ブラウザや他の言語では同じコードは動きません。
実際にBase64/Base64URLを切り替えながらエンコード・デコードを試したい場合は、以下のツールで確認できます。