blob: 8cc3e49939be67da679cb71716d215c9fab11263 [file]
# Copyright 2021 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.
"""JWT testing service API implementations in Python."""
from __future__ import absolute_import
from __future__ import division
# Placeholder for import for type annotations
from __future__ import print_function
import datetime
import json
from typing import Text, Tuple
import grpc
import tink
from tink import cleartext_keyset_handle
from proto.testing import testing_api_pb2
from proto.testing import testing_api_pb2_grpc
from tink import jwt
def _to_timestamp_tuple(t: datetime.datetime) -> Tuple[int, int]:
if not t.tzinfo:
raise ValueError('datetime must have tzinfo')
seconds = int(t.timestamp())
nanos = int((t.timestamp() - seconds) * 1e9)
return (seconds, nanos)
def _from_timestamp_proto(
timestamp: testing_api_pb2.Timestamp) -> datetime.datetime:
t = timestamp.seconds + (timestamp.nanos / 1e9)
return datetime.datetime.fromtimestamp(t, datetime.timezone.utc)
def _from_duration_proto(
duration: testing_api_pb2.Duration) -> datetime.timedelta:
return datetime.timedelta(seconds=duration.seconds)
def raw_jwt_from_proto(proto_raw_jwt: testing_api_pb2.JwtToken) -> jwt.RawJwt:
"""Converts a proto JwtToken into a jwt.RawJwt."""
issuer = None
if proto_raw_jwt.HasField('issuer'):
issuer = proto_raw_jwt.issuer.value
subject = None
if proto_raw_jwt.HasField('subject'):
subject = proto_raw_jwt.subject.value
audiences = list(proto_raw_jwt.audiences)
if not audiences:
audiences = None
jwt_id = None
if proto_raw_jwt.HasField('jwt_id'):
jwt_id = proto_raw_jwt.jwt_id.value
custom_claims = {}
for name, claim in proto_raw_jwt.custom_claims.items():
if claim.HasField('null_value'):
custom_claims[name] = None
elif claim.HasField('number_value'):
custom_claims[name] = claim.number_value
elif claim.HasField('string_value'):
custom_claims[name] = claim.string_value
elif claim.HasField('bool_value'):
custom_claims[name] = claim.bool_value
elif claim.HasField('json_object_value'):
custom_claims[name] = json.loads(claim.json_object_value)
elif claim.HasField('json_array_value'):
custom_claims[name] = json.loads(claim.json_array_value)
else:
raise ValueError('claim %s has unknown type' % name)
return jwt.new_raw_jwt(
issuer=issuer,
subject=subject,
audiences=audiences,
jwt_id=jwt_id,
expiration=_from_timestamp_proto(proto_raw_jwt.expiration),
not_before=_from_timestamp_proto(proto_raw_jwt.not_before),
issued_at=_from_timestamp_proto(proto_raw_jwt.issued_at),
custom_claims=custom_claims)
def verifiedjwt_to_proto(
verified_jwt: jwt.VerifiedJwt) -> testing_api_pb2.JwtToken:
"""Converts a jwt.VerifiedJwt into a proto JwtToken."""
token = testing_api_pb2.JwtToken()
if verified_jwt.has_issuer():
token.issuer.value = verified_jwt.issuer()
if verified_jwt.has_subject():
token.subject.value = verified_jwt.subject()
if verified_jwt.has_audiences():
token.audiences.extend(verified_jwt.audiences())
if verified_jwt.has_jwt_id():
token.jwt_id.value = verified_jwt.jwt_id()
if verified_jwt.has_expiration():
seconds, nanos = _to_timestamp_tuple(verified_jwt.expiration())
token.expiration.seconds = seconds
token.expiration.nanos = nanos
if verified_jwt.has_not_before():
seconds, nanos = _to_timestamp_tuple(verified_jwt.not_before())
token.not_before.seconds = seconds
token.not_before.nanos = nanos
if verified_jwt.has_issued_at():
seconds, nanos = _to_timestamp_tuple(verified_jwt.issued_at())
token.issued_at.seconds = seconds
token.issued_at.nanos = nanos
for name in verified_jwt.custom_claim_names():
value = verified_jwt.custom_claim(name)
if value is None:
token.custom_claims[name].null_value = testing_api_pb2.NULL_VALUE
elif isinstance(value, bool):
token.custom_claims[name].bool_value = value
elif isinstance(value, (int, float)):
token.custom_claims[name].number_value = value
elif isinstance(value, Text):
token.custom_claims[name].string_value = value
elif isinstance(value, dict):
token.custom_claims[name].json_object_value = json.dumps(value)
elif isinstance(value, list):
token.custom_claims[name].json_array_value = json.dumps(value)
else:
raise ValueError('claim %s has unknown type' % name)
return token
def validator_from_proto(
proto_validator: testing_api_pb2.JwtValidator) -> jwt.JwtValidator:
"""Converts a proto JwtValidator into a JwtValidator."""
issuer = None
if proto_validator.HasField('issuer'):
issuer = proto_validator.issuer.value
subject = None
if proto_validator.HasField('subject'):
subject = proto_validator.subject.value
audience = None
if proto_validator.HasField('audience'):
audience = proto_validator.audience.value
return jwt.new_validator(
issuer=issuer,
subject=subject,
audience=audience,
fixed_now=_from_timestamp_proto(proto_validator.now),
clock_skew=_from_duration_proto(proto_validator.clock_skew))
class JwtServicer(testing_api_pb2_grpc.JwtServicer):
"""A service for signing and verifying JWTs."""
def ComputeMacAndEncode(
self, request: testing_api_pb2.JwtSignRequest,
context: grpc.ServicerContext) -> testing_api_pb2.JwtSignResponse:
"""Computes a MACed compact JWT."""
try:
keyset_handle = cleartext_keyset_handle.read(
tink.BinaryKeysetReader(request.keyset))
p = keyset_handle.primitive(jwt.JwtMac)
raw_jwt = raw_jwt_from_proto(request.raw_jwt)
signed_compact_jwt = p.compute_mac_and_encode(raw_jwt)
return testing_api_pb2.JwtSignResponse(
signed_compact_jwt=signed_compact_jwt)
except tink.TinkError as e:
return testing_api_pb2.JwtSignResponse(err=str(e))
def VerifyMacAndDecode(
self, request: testing_api_pb2.JwtVerifyRequest,
context: grpc.ServicerContext) -> testing_api_pb2.JwtVerifyResponse:
"""Verifies a MAC value."""
try:
keyset_handle = cleartext_keyset_handle.read(
tink.BinaryKeysetReader(request.keyset))
validator = validator_from_proto(request.validator)
p = keyset_handle.primitive(jwt.JwtMac)
verified_jwt = p.verify_mac_and_decode(request.signed_compact_jwt,
validator)
return testing_api_pb2.JwtVerifyResponse(
verified_jwt=verifiedjwt_to_proto(verified_jwt))
except tink.TinkError as e:
return testing_api_pb2.JwtVerifyResponse(err=str(e))
def PublicKeySignAndEncode(
self, request: testing_api_pb2.JwtSignRequest,
context: grpc.ServicerContext) -> testing_api_pb2.JwtSignResponse:
"""Computes a signed compact JWT token."""
return testing_api_pb2.JwtSignResponse(err='Not yet implemented.')
def PublicKeyVerifyAndDecode(
self, request: testing_api_pb2.JwtVerifyRequest,
context: grpc.ServicerContext) -> testing_api_pb2.JwtVerifyResponse:
"""Verifies the validity of the signed compact JWT token."""
return testing_api_pb2.JwtVerifyResponse(err='Not yet implemented.')