go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth/jwt/jwt.go (about) 1 // Copyright 2021 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package jwt contains low-level utilities for verifying JSON Web Tokens. 16 // 17 // Supports only non-encrypted RS256 tokens with `kid` header field populated, 18 // as produced by Google Cloud Platform. 19 package jwt 20 21 import ( 22 "encoding/base64" 23 "encoding/json" 24 "strings" 25 26 "go.chromium.org/luci/common/errors" 27 ) 28 29 // NotJWT is an error tag used to indicate that the string passed to 30 // VerifyAndDecode is not in fact structurally a JWT. 31 var NotJWT = errors.BoolTag{Key: errors.NewTagKey("not a JSON web token")} 32 33 // SignatureVerifier can verify RS256 signatures. 34 type SignatureVerifier interface { 35 // CheckSignature returns nil if `signed` was indeed signed by given key. 36 CheckSignature(keyID string, signed, signature []byte) error 37 } 38 39 // UnsafeDecode extracts the payload of a JWT **without verifying it**. 40 // 41 // It must always be followed by VerifyAndDecode. Useful to "peek" inside the 42 // token to see who it was supposedly signed by. 43 func UnsafeDecode(jwt string, dest any) error { 44 chunks := strings.Split(jwt, ".") 45 if len(chunks) != 3 { 46 return errors.Reason("bad JWT: expected 3 components separated by '.'").Tag(NotJWT).Err() 47 } 48 if err := unmarshalB64JSON(chunks[1], dest); err != nil { 49 return errors.Annotate(err, "bad JWT: bad body").Err() 50 } 51 return nil 52 } 53 54 // VerifyAndDecode deconstructs the token, verifies its signature using the 55 // given `verifier` and on success deserializes its body into `dest`. 56 // 57 // Returns errors tagged with NotJWT if `token` doesn't look like a JWT at 58 // all. Other errors (like signature verification check errors) are returned 59 // without this tag. 60 // 61 // Doesn't interpret any JWT claims in the body, just deserializes them into 62 // `dest`. The caller is responsible for checking them. 63 func VerifyAndDecode(jwt string, dest any, verifier SignatureVerifier) error { 64 chunks := strings.Split(jwt, ".") 65 if len(chunks) != 3 { 66 return errors.Reason("bad JWT: expected 3 components separated by '.'").Tag(NotJWT).Err() 67 } 68 69 // Check we've got the supported kind of token. 70 var hdr struct { 71 Alg string `json:"alg"` 72 Kid string `json:"kid"` 73 } 74 if err := unmarshalB64JSON(chunks[0], &hdr); err != nil { 75 return errors.Annotate(err, "bad JWT header").Tag(NotJWT).Err() 76 } 77 if hdr.Alg != "RS256" { 78 return errors.Reason("bad JWT: only RS256 alg is supported, not %q", hdr.Alg).Err() 79 } 80 if hdr.Kid == "" { 81 return errors.Reason("bad JWT: missing the signing key ID in the header").Err() 82 } 83 84 // Decode the signature. 85 sig, err := base64.RawURLEncoding.DecodeString(chunks[2]) 86 if err != nil { 87 return errors.Annotate(err, "bad JWT: can't base64 decode the signature").Err() 88 } 89 90 // Check the signature. The signed string is "b64(header).b64(body)". 91 signed := chunks[0] + "." + chunks[1] 92 if err := verifier.CheckSignature(hdr.Kid, []byte(signed), sig); err != nil { 93 return errors.Annotate(err, "bad JWT: signature check error").Err() 94 } 95 96 // Decode and deserialize the body. There should be no errors here generally, 97 // the encoded body is signed and the signature was already verified. 98 if err := unmarshalB64JSON(chunks[1], dest); err != nil { 99 return errors.Annotate(err, "bad JWT: bad body").Err() 100 } 101 return nil 102 } 103 104 func unmarshalB64JSON(blob string, out any) error { 105 raw, err := base64.RawURLEncoding.DecodeString(blob) 106 if err != nil { 107 return errors.Annotate(err, "not base64").Err() 108 } 109 if err := json.Unmarshal(raw, out); err != nil { 110 return errors.Annotate(err, "can't deserialize JSON").Err() 111 } 112 return nil 113 }