lib/ga4gh/visa.go (106 lines of code) (raw):
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ga4gh
import (
"context"
"fmt"
"github.com/go-jose/go-jose/v3/jwt" /* copybara-comment */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/kms" /* copybara-comment: kms */
glog "github.com/golang/glog" /* copybara-comment */
cpb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/common/v1" /* copybara-comment: go_proto */
)
// Visa represents a GA4GH Passport Visa.
// A Visa is a "Signed Assertion".
type Visa struct {
// jwt for the visa.
jwt VisaJWT
// "jku" visa header (see https://tools.ietf.org/html/rfc7515#section-4.1.2).
// This header changes the visa type as per the GA4GH AAI Profile
// (https://github.com/ga4gh/data-security/blob/master/AAI/AAIConnectProfile.md#term-embedded-document-token).
jku string
// data is unmarhsalled data contained in visa jwt.
data *VisaData
}
// VisaJWT is a JWT object containing a GA4GH Visa.
type VisaJWT string
// VisaData is used for creating a new visa.
type VisaData struct {
// StdClaims is embeded for standard JWT claims.
StdClaims
// Scope for the Visa.
// http://bit.ly/ga4gh-aai-profile#ga4gh-jwt-format
Scope Scope `json:"scope,omitempty"`
// Assertion contains the Visa Assertion.
Assertion Assertion `json:"ga4gh_visa_v1,omitempty"`
}
// NewVisaFromJWT creates a new Visa from a given JWT.
// Returns error if the JWT is not the JWT of a Visa.
// Does not verify the signature on the JWT.
func NewVisaFromJWT(j VisaJWT) (*Visa, error) {
glog.V(1).Infof("NewVisaFromJWT(%+v)", j)
d, jku, err := visaDataFromJWT(j)
if err != nil {
return nil, err
}
return &Visa{
jwt: j,
jku: jku,
data: d,
}, nil
}
// NewVisaFromData creates a new Visa.
//
// keyID identifies the key used by issuer to sign the JWT.
// Visit the issuer's JWKS endpoint to obtain the keys and find the public key corresponding to the keyID.
// To find the issuer's JWKS endpoint:
//
// If openid scope exists in the Visa, visit "[issuer]/.well-known/openid-configuration"
// Else if "jku" exists in JWT header, use the "jku" value.
// Otherwise, the Visa cannot be verified.
//
// See https://bit.ly/ga4gh-aai-profile#embedded-token-issued-by-embedded-token-issuer
func NewVisaFromData(ctx context.Context, d *VisaData, jku string, signer kms.Signer) (*Visa, error) {
j, err := visaJWTFromData(ctx, d, jku, signer)
if err != nil {
return nil, err
}
return &Visa{
jwt: j,
jku: jku,
data: d,
}, nil
}
// JKU returns the JKU header of a Visa.
func (v *Visa) JKU() string {
return v.jku
}
// JWT returns the JWT of a Visa.
func (v *Visa) JWT() VisaJWT {
return v.jwt
}
// Data returns the data of a Visa.
func (v *Visa) Data() *VisaData {
return v.data
}
// AssertionProto returns the visa assertion in common proto (cpb.Assertion) format.
func (v *Visa) AssertionProto() *cpb.Assertion {
a := toAssertionProto(v.data.Assertion)
a.Exp = v.Data().ExpiresAt
return a
}
// Format returns the VisaFormat (i.e. embedded token format) for the visa.
func (v *Visa) Format() VisaFormat {
if len(v.jku) == 0 {
return AccessTokenVisaFormat
}
return DocumentVisaFormat
}
func visaJWTFromData(ctx context.Context, d *VisaData, jku string, signer kms.Signer) (VisaJWT, error) {
header := map[string]string{}
if jku != JWTEmptyJKU {
header[jwtHeaderJKU] = jku
}
signed, err := signer.SignJWT(ctx, d, header)
if err != nil {
return "", err
}
return VisaJWT(signed), nil
}
// visaDataFromJWT converts a JWT token to data elements.
// Returns: visa payload data, the "jku" header string (if any), and error.
func visaDataFromJWT(j VisaJWT) (*VisaData, string, error) {
d := &VisaData{}
tok, err := jwt.ParseSigned(string(j))
if err != nil {
return nil, JWTEmptyJKU, fmt.Errorf("ParseSigned() failed: %v", err)
}
if err := tok.UnsafeClaimsWithoutVerification(d); err != nil {
return nil, JWTEmptyJKU, fmt.Errorf("UnsafeClaimsWithoutVerification() failed: %v", err)
}
if len(tok.Headers) != 1 {
return nil, JWTEmptyJKU, fmt.Errorf("jwt invalid header")
}
if len(tok.Headers[0].ExtraHeaders) == 0 {
return d, JWTEmptyJKU, nil
}
jku, ok := tok.Headers[0].ExtraHeaders["jku"]
if !ok {
return d, JWTEmptyJKU, nil
}
str, ok := jku.(string)
if !ok {
return nil, JWTEmptyJKU, fmt.Errorf("casting jku to string failed")
}
return d, str, nil
}
// MustVisaDataFromJWT converts a VisaJWT to VisaData.
// Crashes if VisaJWT cannot be parsed.
// Useful for writing tests: cmp.Transformer("", ga4gh.VisaJWTTransform)
// DO NOT use in non-test code.
// TODO: move to a testutil package.
func MustVisaDataFromJWT(j VisaJWT) *VisaData {
d, _, err := visaDataFromJWT(j)
if err != nil {
glog.Fatalf("visaDataFromJWT(%v) failed: %v", j, err)
}
return d
}