File: security.hpp

package info (click to toggle)
libzeep 5.1.8-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 3,596 kB
  • sloc: cpp: 27,393; xml: 7,798; javascript: 180; sh: 37; makefile: 8
file content (304 lines) | stat: -rw-r--r-- 10,112 bytes parent folder | download
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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
//        Copyright Maarten L. Hekkelman, 2014-2022
//   Distributed under the Boost Software License, Version 1.0.
//      (See accompanying file LICENSE_1_0.txt or copy at
//            http://www.boost.org/LICENSE_1_0.txt)

#pragma once

/// \file
/// definition of various classes that help in handling HTTP authentication.

#include <zeep/config.hpp>

#include <set>

#include <zeep/exception.hpp>
#include <zeep/crypto.hpp>
#include <zeep/http/server.hpp>
#include <zeep/json/element.hpp>

// --------------------------------------------------------------------
//

namespace zeep::http
{

/// \brief exception thrown when unauthorized access is detected
///
/// when using authentication, this exception is thrown for unauthorized access

struct unauthorized_exception : public zeep::exception
{
	/// \brief constructor
	unauthorized_exception() : exception("unauthorized") { }
};

// --------------------------------------------------------------------

class password_encoder
{
  public:
	virtual ~password_encoder() {}

	virtual std::string encode(const std::string& password) const = 0;
	virtual bool matches(const std::string& raw_password, const std::string& stored_password) const = 0;
};

// --------------------------------------------------------------------

class pbkdf2_sha256_password_encoder : public password_encoder
{
  public:
	static inline constexpr const char* name() { return "pbkdf2_sha256"; };

	pbkdf2_sha256_password_encoder(int iterations = 30000, int key_length = 32)
		: m_iterations(iterations), m_key_length(key_length) {}

	virtual std::string encode(const std::string& password) const
	{
		using namespace std::literals;

		auto salt = zeep::encode_base64(zeep::random_hash()).substr(12);
		auto pw = zeep::encode_base64(zeep::pbkdf2_hmac_sha256(salt, password, m_iterations, m_key_length));
		return "pbkdf2_sha256$" + std::to_string(m_iterations) + '$' + salt + '$' + pw;
	}

	virtual bool matches(const std::string& raw_password, const std::string& stored_password) const
	{
		using namespace std::literals;

		bool result = false;

		std::regex rx(R"(pbkdf2_sha256\$(\d+)\$([^$]+)\$(.+))");

		std::smatch m;
		if (std::regex_match(stored_password, m, rx))
		{
			auto salt = m[2].str();
			auto iterations = std::stoul(m[1]);

			auto test = zeep::pbkdf2_hmac_sha256(salt, raw_password, iterations, m_key_length);
			test = zeep::encode_base64(test);

			result = (m[3] == test);
		}

		return result;
	}

  private:
	int m_iterations, m_key_length;
};

// --------------------------------------------------------------------

/// \brief simple storage class for user details, returned by user_service
///
/// The user_details struct contains all the information needed to allow
/// access to a resource based on username. The password is the encrypted
/// password.
struct user_details
{
	user_details() {}
	user_details(const std::string& username, const std::string& password, const std::set<std::string>& roles)
		: username(username), password(password), roles(roles) {}

	std::string username;
	std::string password;
	std::set<std::string> roles;
};

// --------------------------------------------------------------------

/// \brief exception thrown by user_service when trying to load user_details for an unknown user
class user_unknown_exception : public zeep::exception
{
  public:
	user_unknown_exception() : zeep::exception("user unknown") {};
};

/// \brief exception thrown by security_context when a username/password combo is not valid
class invalid_password_exception : public zeep::exception
{
  public:
	invalid_password_exception() : zeep::exception("invalid password") {};
};

// --------------------------------------------------------------------

/// \brief The user service class, provding user data used for authentication
///
/// This is an abstract base class for a user service.

class user_service
{
  public:
	user_service() {}
	virtual ~user_service() {}

	/// \brief return the user_details for a user named \a username
	virtual user_details load_user(const std::string& username) const = 0;
};

// --------------------------------------------------------------------

/// \brief A very simple implementation of the user service class
///
/// This implementation of a user service can be used to jump start a
/// project. Normally you would implement something more robust.

class simple_user_service : public user_service
{
  public:
	simple_user_service(std::initializer_list<std::tuple<std::string,std::string,std::set<std::string>>> users)
	{
		for (auto const& [username, password, roles]: users)
			add_user(username, password, roles);
	}

	/// \brief return the user_details for a user named \a username
	virtual user_details load_user(const std::string& username) const
	{
		user_details result = {};
		auto ui = std::find_if(m_users.begin(), m_users.end(), [username](const user_details& u) { return u.username == username; });
		if (ui != m_users.end())
			result = *ui;
		return result;
	}

	void add_user(const std::string& username, const std::string& password, const std::set<std::string>& roles)
	{
		m_users.emplace_back(username, password, roles);
	}

  protected:
	std::vector<user_details> m_users;
};

// --------------------------------------------------------------------

/// \brief class that manages security in a HTTP scope
///
/// Add this to a HTTP server and it will check authentication.
/// Access to certain paths can be limited by specifying which
/// 'roles' are allowed.
///
/// The authentication mechanism used is based on JSON Web Tokens, JWT in short.

class security_context
{
  public:
	/// \brief constructor taking a validator
	///
	/// Create a security context for server \a s with validator \a validator and
	/// a flag \a defaultAccessAllowed indicating if non-matched uri's should be allowed
	security_context(const std::string& secret, user_service& users, bool defaultAccessAllowed = false)
		: m_secret(secret), m_users(users), m_default_allow(defaultAccessAllowed)
	{
		register_password_encoder<pbkdf2_sha256_password_encoder>();
	}

	/// \brief register a custom password encoder
	///
	/// The password encoder should derive from the abstract password encoder class above
	/// and also implement the name() method.
	template<typename PWEncoder>
	void register_password_encoder()
	{
		m_known_password_encoders.emplace_back(PWEncoder::name(), new PWEncoder());
	}

	/// \brief Add a new rule for access
	///
	/// A new rule will be added to the list, allowing access to \a glob_pattern
	/// to users having role \a role
	///
 	/// \a glob_pattern should start with a slash
	void add_rule(const std::string& glob_pattern, const std::string& role)
	{
		add_rule(glob_pattern, { role });
	}

	/// \brief Add a new rule for access
	///
	/// A new rule will be added to the list, allowing access to \a glob_pattern
	/// to users having a role in \a roles
	///
	/// If \a roles is empty, access is allowed to anyone.
	///
	/// \a glob_pattern should start with a slash
	void add_rule(std::string glob_pattern, std::initializer_list<std::string> roles)
	{
		assert(glob_pattern.front() == '/');
		m_rules.emplace_back(rule{glob_pattern, roles});
	}

	/// \brief Validate the request \a req against the stored rules
	///
	/// This method will validate the request in \a req agains the stored rules
	/// and will throw an exception if access is not allowed.
	/// The request \a req will be updated with the credentials for further use.
	/// If the validate CSRF is set, the CSRF token will also be validated.
	void validate_request(request& req) const;

	/// \brief Add e.g. headers to reply for an authorized request
	///
	/// When validation succeeds, a HTTP reply is send to the user and this routine will be
	/// called to augment the reply with additional information.
	///
	/// \param rep			Then zeep::http::reply object that will be send to the user
	/// \param username		The name for the authorized user, credentials will be fetched from the user_service
	void add_authorization_headers(reply &rep, const std::string& username);

	/// \brief Add e.g. headers to reply for an authorized request
	///
	/// When validation succeeds, a HTTP reply is send to the user and this routine will be
	/// called to augment the reply with additional information.
	///
	/// \param rep			Then zeep::http::reply object that will be send to the user
	/// \param user			The authorized user details
	void add_authorization_headers(reply &rep, const user_details user);

	/// \brief verify the username/password combination and set a cookie in the reply in case of success
	///
	/// When validation succeeds, add_authorization_headers is called, otherwise an exception is thrown.
	///
	/// \param username		The name for the user
	/// \param password		The password for the user
	/// \param rep			Then zeep::http::reply object that will be send back to the browser
	void verify_username_password(const std::string& username, const std::string& password, reply &rep);

	/// \brief return reference to the user_service object
	user_service& get_user_service() const			{ return m_users; }

	/// \brief Get or create a CSRF token for the current request
	///
	/// Return a CSRF token. If this was not present in the request, a new will be generated
	/// \param req		The HTTP request
	/// \return			A std::pair containing the CSRF token and a flag indicating the token is new
	std::pair<std::string,bool> get_csrf_token(request& req);

	/// \brief To automatically validate CSRF tokens, set this flag
	void set_validate_csrf(bool validate)			{ m_validate_csrf = validate; }
	bool get_validate_csrf() const					{ return m_validate_csrf; }

  private:
	security_context(const security_context&) = delete;
	security_context& operator=(const security_context&) = delete;

	struct rule
	{
		std::string				m_pattern;
		std::set<std::string>	m_roles;
	};

	std::string m_secret;
	user_service& m_users;
	bool m_default_allow;
	bool m_validate_csrf = false;
	std::vector<rule> m_rules;
	std::vector<std::tuple<std::string,std::unique_ptr<password_encoder>>> m_known_password_encoders;
};


} // namespace zeep::http