forked from microsoft/CCF
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjwt_auth.cpp
214 lines (184 loc) · 5.96 KB
/
jwt_auth.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
#include "ccf/endpoints/authentication/jwt_auth.h"
#include "ccf/ds/nonstd.h"
#include "ccf/pal/locking.h"
#include "ccf/rpc_context.h"
#include "ccf/service/tables/jwt.h"
#include "ds/lru.h"
#include "enclave/enclave_time.h"
#include "http/http_jwt.h"
namespace
{
static const std::string multitenancy_indicator{"{tenantid}"};
static const std::string microsoft_entra_domain{"login.microsoftonline.com"};
std::optional<std::string_view> first_non_empty_chunk(
const std::vector<std::string_view>& chunks)
{
for (auto chunk : chunks)
{
if (!chunk.empty())
{
return chunk;
}
}
return std::nullopt;
}
}
namespace ccf
{
bool validate_issuer(
const std::string& iss,
const std::optional<std::string>& tid,
std::string constraint)
{
LOG_DEBUG_FMT(
"Verify token.iss {} and token.tid {} against published key issuer {}",
iss,
tid,
constraint);
const auto issuer_url = ::http::parse_url_full(constraint);
if (issuer_url.host != microsoft_entra_domain)
{
return iss == constraint &&
!tid; // tid is a MSFT-specific claim and
// shoudn't be set for a non-Entra issuer.
}
// Specify tenant if working with multi-tenant endpoint.
const auto pos = constraint.find(multitenancy_indicator);
if (pos != std::string::npos && tid)
{
constraint.replace(pos, multitenancy_indicator.size(), *tid);
}
// Step 1. Verify the token issuer against the key issuer.
if (iss != constraint)
{
return false;
}
// Step 2. Verify that token.tid is served as a part of token.iss. According
// to the documentation, we only accept this format:
//
// https://domain.com/tenant_id/something_else
//
// Here url.path == "/tenant_id/something_else".
//
// Check for details here:
// https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens#validate-the-issuer.
const auto url = ::http::parse_url_full(iss);
const auto tenant_id =
first_non_empty_chunk(ccf::nonstd::split(url.path, "/"));
return tenant_id && tid && *tid == *tenant_id;
}
struct VerifiersCache
{
static constexpr size_t DEFAULT_MAX_VERIFIERS = 10;
using DER = std::vector<uint8_t>;
ccf::pal::Mutex verifiers_lock;
LRU<DER, ccf::crypto::VerifierPtr> verifiers;
VerifiersCache(size_t max_verifiers = DEFAULT_MAX_VERIFIERS) :
verifiers(max_verifiers)
{}
ccf::crypto::VerifierPtr get_verifier(const DER& der)
{
std::lock_guard<ccf::pal::Mutex> guard(verifiers_lock);
auto it = verifiers.find(der);
if (it == verifiers.end())
{
it = verifiers.insert(der, ccf::crypto::make_unique_verifier(der));
}
return it->second;
}
};
JwtAuthnPolicy::JwtAuthnPolicy() :
verifiers(std::make_unique<VerifiersCache>())
{}
JwtAuthnPolicy::~JwtAuthnPolicy() = default;
std::unique_ptr<AuthnIdentity> JwtAuthnPolicy::authenticate(
ccf::kv::ReadOnlyTx& tx,
const std::shared_ptr<ccf::RpcContext>& ctx,
std::string& error_reason)
{
const auto& headers = ctx->get_request_headers();
const auto token_opt =
::http::JwtVerifier::extract_token(headers, error_reason);
if (!token_opt)
{
error_reason = "Invalid JWT token";
return nullptr;
}
auto& token = token_opt.value();
auto keys = tx.ro<JwtPublicSigningKeys>(
ccf::Tables::JWT_PUBLIC_SIGNING_KEYS_METADATA);
const auto key_id = token.header_typed.kid;
auto token_keys = keys->get(key_id);
if (!token_keys)
{
error_reason =
fmt::format("JWT signing key not found for kid {}", key_id);
return nullptr;
}
for (const auto& metadata : *token_keys)
{
auto verifier = verifiers->get_verifier(metadata.cert);
if (!::http::JwtVerifier::validate_token_signature(token, verifier))
{
continue;
}
// Check that the Not Before and Expiration Time claims are valid
const size_t time_now = std::chrono::duration_cast<std::chrono::seconds>(
ccf::get_enclave_time())
.count();
if (time_now < token.payload_typed.nbf)
{
error_reason = fmt::format(
"Current time {} is before token's Not Before (nbf) claim {}",
time_now,
token.payload_typed.nbf);
}
else if (time_now > token.payload_typed.exp)
{
error_reason = fmt::format(
"Current time {} is after token's Expiration Time (exp) claim {}",
time_now,
token.payload_typed.exp);
}
else if (
metadata.constraint &&
!validate_issuer(
token.payload_typed.iss,
token.payload_typed.tid,
*metadata.constraint))
{
error_reason = fmt::format(
"Kid {} failed issuer constraint validation {}",
key_id,
*metadata.constraint);
}
else
{
auto identity = std::make_unique<JwtAuthnIdentity>();
identity->key_issuer = metadata.issuer;
identity->header = std::move(token.header);
identity->payload = std::move(token.payload);
return identity;
}
}
error_reason = "Can't authenticate JWT token";
return nullptr;
}
void JwtAuthnPolicy::set_unauthenticated_error(
std::shared_ptr<ccf::RpcContext> ctx, std::string&& error_reason)
{
ctx->set_error(
HTTP_STATUS_UNAUTHORIZED,
ccf::errors::InvalidAuthenticationInfo,
std::move(error_reason));
ctx->set_response_header(
http::headers::WWW_AUTHENTICATE,
"Bearer realm=\"JWT bearer token access\", error=\"invalid_token\"");
}
const OpenAPISecuritySchema JwtAuthnPolicy::security_schema = std::make_pair(
JwtAuthnPolicy::SECURITY_SCHEME_NAME,
nlohmann::json{
{"type", "http"}, {"scheme", "bearer"}, {"bearerFormat", "JWT"}});
}