Mass refactor. Clang-format. Start adding tests. Use FetchContent.
This commit is contained in:
1 parent
c76ff553cc
commit
3309b19c2e
19 files changed
+310
-223
No files matched your search
+10
-12
@@ -11,39 +11,37 @@
|
||||
// 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.
|
||||
#include <j7s-plugin/AuthList.hpp>
|
||||
#include <algorithm>
|
||||
#include <j7s-plugin/AuthList.hpp>
|
||||
|
||||
AuthList::AuthList():
|
||||
_allowedUsernames{}
|
||||
{
|
||||
}
|
||||
AuthList::AuthList() : _allowedUsernames{} {}
|
||||
|
||||
void AuthList::add(const std::string& username)
|
||||
void AuthList::add(const std::string & username)
|
||||
{
|
||||
// Is the username already in the list?
|
||||
// If not add it.
|
||||
if(not confirm(username))
|
||||
if (not confirm(username))
|
||||
{
|
||||
_allowedUsernames.emplace_front(username);
|
||||
}
|
||||
}
|
||||
|
||||
void AuthList::remove(const std::string& username)
|
||||
void AuthList::remove(const std::string & username)
|
||||
{
|
||||
// Is the user in the list?
|
||||
// Is so, remove it,
|
||||
if(confirm(username))
|
||||
if (confirm(username))
|
||||
{
|
||||
_allowedUsernames.remove(username);
|
||||
}
|
||||
}
|
||||
|
||||
bool AuthList::confirm(const std::string& username)
|
||||
bool AuthList::confirm(const std::string & username)
|
||||
{
|
||||
// Is the user in the list?
|
||||
const auto found = std::find(std::begin(_allowedUsernames), std::end(_allowedUsernames), username);
|
||||
if(found != std::end(_allowedUsernames))
|
||||
const auto found =
|
||||
std::find(std::begin(_allowedUsernames), std::end(_allowedUsernames), username);
|
||||
if (found != std::end(_allowedUsernames))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
+15
-70
@@ -11,95 +11,41 @@
|
||||
// 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.
|
||||
#include <j7s-plugin/Authorizer.hpp>
|
||||
#include <j7s-plugin/AuthList.hpp>
|
||||
|
||||
#include <jwt-cpp/jwt.h>
|
||||
#include <j7s-plugin/utils.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <j7s-plugin/AuthList.hpp>
|
||||
#include <j7s-plugin/Authorizer.hpp>
|
||||
|
||||
Authorizer::Authorizer(const std::string& pub_key, const std::string& issuer):
|
||||
_pub_key{pub_key},
|
||||
_issuer{issuer}
|
||||
Authorizer::Authorizer(const std::string & pub_key, const std::string & issuer) :
|
||||
_pub_key{pub_key}, _issuer{issuer}
|
||||
{
|
||||
}
|
||||
|
||||
std::optional<std::string> Authorizer::read_key(const std::string& key_file)
|
||||
{
|
||||
// Read key from file.
|
||||
std::ifstream key_stream(key_file, std::ios::binary);
|
||||
if(not key_stream)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
std::stringstream ss;
|
||||
ss << key_stream.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
void Authorizer::add_unknown(const std::string& username)
|
||||
void Authorizer::add_unknown(const std::string & username)
|
||||
{
|
||||
_unknownList.add(username);
|
||||
}
|
||||
|
||||
bool Authorizer::is_unknown(const std::string& username)
|
||||
bool Authorizer::is_unknown(const std::string & username)
|
||||
{
|
||||
return (username.empty() or _unknownList.confirm(username));
|
||||
}
|
||||
|
||||
bool Authorizer::add(const std::string& token, const std::string& username)
|
||||
bool Authorizer::add(const std::string & token, const std::string & username)
|
||||
{
|
||||
const auto decoded_token = jwt::decode(token);
|
||||
|
||||
// Is the token valid?
|
||||
const auto verifier = jwt::verify()
|
||||
.with_issuer(_issuer)
|
||||
.allow_algorithm(jwt::algorithm::rs256(_pub_key));
|
||||
try
|
||||
{
|
||||
verifier.verify(decoded_token);
|
||||
}
|
||||
catch(jwt::error::token_verification_exception& exception)
|
||||
{
|
||||
std::cerr << exception.what() << std::endl;
|
||||
return false;
|
||||
}
|
||||
auto claims = decoded_token.get_payload_claims();
|
||||
|
||||
// Check username matches.
|
||||
if(not claims.contains("upn"))
|
||||
{
|
||||
std::cerr << "Missing upn." << std::endl;
|
||||
return false;
|
||||
}
|
||||
if(claims["upn"].as_string() != username)
|
||||
{
|
||||
std::cerr << "Wrong username." << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for mqtt-write claim value.
|
||||
if(not (claims.contains("mqtt-write") and claims.contains("mqtt-read")))
|
||||
{
|
||||
std::cerr << "Missing mqtt-write or mqtt-read." << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool can_write = claims["mqtt-write"].as_bool();
|
||||
bool can_read = claims["mqtt-read"].as_bool();
|
||||
if(not (can_write or can_read))
|
||||
const auto [can_read, can_write] = validate(token, username, _issuer, _pub_key);
|
||||
if (not(can_write or can_read))
|
||||
{
|
||||
std::cerr << "Can't write or can't read." << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(can_write)
|
||||
if (can_write)
|
||||
{
|
||||
_writeList.add(username);
|
||||
}
|
||||
if(can_read)
|
||||
if (can_read)
|
||||
{
|
||||
_readList.add(username);
|
||||
}
|
||||
@@ -107,20 +53,19 @@ bool Authorizer::add(const std::string& token, const std::string& username)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Authorizer::can_read(const std::string& username)
|
||||
bool Authorizer::can_read(const std::string & username)
|
||||
{
|
||||
return _readList.confirm(username);
|
||||
}
|
||||
|
||||
bool Authorizer::can_write(const std::string& username)
|
||||
bool Authorizer::can_write(const std::string & username)
|
||||
{
|
||||
return _writeList.confirm(username);
|
||||
}
|
||||
|
||||
void Authorizer::logout(const std::string& username)
|
||||
void Authorizer::logout(const std::string & username)
|
||||
{
|
||||
_writeList.remove(username);
|
||||
_readList.remove(username);
|
||||
_unknownList.remove(username);
|
||||
}
|
||||
|
||||
+16
-44
@@ -12,35 +12,20 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include <argparse/argparse.hpp>
|
||||
#include <jwt-cpp/jwt.h>
|
||||
#include <j7s-plugin/utils.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <argparse/argparse.hpp>
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <filesystem>
|
||||
|
||||
std::optional<std::string> read_key(const std::string& key_file);
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
int main(int argc, char * argv[])
|
||||
{
|
||||
argparse::ArgumentParser program("gen-token", "0.0.0");
|
||||
|
||||
program.add_argument("--pub-key")
|
||||
.required()
|
||||
.help("Pub key of signer.");
|
||||
program.add_argument("--priv-key")
|
||||
.required()
|
||||
.help("Private key of signer.");
|
||||
program.add_argument("--issuer")
|
||||
.required()
|
||||
.help("Issuer to assign to signed key.");
|
||||
program.add_argument("--username")
|
||||
.required()
|
||||
.help("Username assigned to key.");
|
||||
program.add_argument("--pub-key").required().help("Pub key of signer.");
|
||||
program.add_argument("--priv-key").required().help("Private key of signer.");
|
||||
program.add_argument("--issuer").required().help("Issuer to assign to signed key.");
|
||||
program.add_argument("--username").required().help("Username assigned to key.");
|
||||
program.add_argument("--valid-days")
|
||||
.required()
|
||||
.help("Days from now until the token will be valid.");
|
||||
@@ -68,7 +53,7 @@ int main(int argc, char *argv[])
|
||||
const auto pub_key_file = program.get<std::string>("--pub-key");
|
||||
const auto pub_key = read_key(std::filesystem::absolute(pub_key_file));
|
||||
|
||||
if(not pub_key or not priv_key)
|
||||
if (not pub_key or not priv_key)
|
||||
{
|
||||
std::cerr << "Could not open key!" << std::endl;
|
||||
return -2;
|
||||
@@ -80,29 +65,16 @@ int main(int argc, char *argv[])
|
||||
const auto expr_time = std::chrono::system_clock::now() +
|
||||
std::chrono::days(std::stoi(program.get<std::string>("--valid-days")));
|
||||
|
||||
const auto token = jwt::create()
|
||||
.set_type("JWT")
|
||||
.set_issuer(program.get<std::string>("--issuer"))
|
||||
.set_payload_claim("upn", jwt::claim(program.get<std::string>("--username")))
|
||||
.set_payload_claim("mqtt-write", jwt::claim(can_read))
|
||||
.set_payload_claim("mqtt-read", jwt::claim(can_write))
|
||||
.set_expires_at(expr_time)
|
||||
.sign(jwt::algorithm::rs256(pub_key.value(), priv_key.value(), "", ""));
|
||||
const auto token = gen_token(
|
||||
program.get<std::string>("--issuer"),
|
||||
program.get<std::string>("--username"),
|
||||
can_read,
|
||||
can_write,
|
||||
pub_key.value(),
|
||||
priv_key.value(),
|
||||
expr_time);
|
||||
|
||||
std::cout << token;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::optional<std::string> read_key(const std::string& key_file)
|
||||
{
|
||||
// Read key from file.
|
||||
std::ifstream key_stream(key_file, std::ios::binary);
|
||||
if(not key_stream)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
std::stringstream ss;
|
||||
ss << key_stream.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
+51
-41
@@ -12,32 +12,36 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
#include <j7s-plugin/j7s-plugin.h>
|
||||
|
||||
#include <j7s-plugin/Authorizer.hpp>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
// Mosquitto Globals
|
||||
static mosquitto_plugin_id_t *plugin_id = nullptr;
|
||||
static mosquitto_plugin_id_t * plugin_id = nullptr;
|
||||
static std::unique_ptr<Authorizer> authorizer = nullptr;
|
||||
|
||||
int mosquitto_plugin_version(int supported_version_count, const int *supported_versions)
|
||||
int mosquitto_plugin_version(int supported_version_count, const int * supported_versions)
|
||||
{
|
||||
for(int index = 0; index < supported_version_count; index++)
|
||||
for (int index = 0; index < supported_version_count; index++)
|
||||
{
|
||||
if(supported_versions[index] == 5)
|
||||
if (supported_versions[index] == 5)
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int mosquitto_plugin_init(mosquitto_plugin_id_t *identifier, void **userdata, struct mosquitto_opt *options, int option_count)
|
||||
int mosquitto_plugin_init(
|
||||
mosquitto_plugin_id_t * identifier,
|
||||
void ** userdata,
|
||||
struct mosquitto_opt * options,
|
||||
int option_count)
|
||||
{
|
||||
plugin_id = identifier;
|
||||
|
||||
|
||||
if(option_count != 2)
|
||||
if (option_count != 2)
|
||||
{
|
||||
mosquitto_log_printf(MOSQ_LOG_ERR, "Missing an option. Found: %d", option_count);
|
||||
return MOSQ_ERR_INVAL;
|
||||
@@ -45,23 +49,23 @@ int mosquitto_plugin_init(mosquitto_plugin_id_t *identifier, void **userdata, st
|
||||
|
||||
std::string public_key;
|
||||
std::string issuer;
|
||||
for(int index = 0; index < option_count; index++)
|
||||
for (int index = 0; index < option_count; index++)
|
||||
{
|
||||
const auto key = std::string(options[index].key);
|
||||
if(key == "public_key")
|
||||
if (key == "public_key")
|
||||
{
|
||||
const auto key = Authorizer::read_key(std::string(options[index].value));
|
||||
if(not key or key->empty())
|
||||
if (not key or key->empty())
|
||||
{
|
||||
mosquitto_log_printf(MOSQ_LOG_ERR, "Could not read public key.");
|
||||
return MOSQ_ERR_INVAL;
|
||||
}
|
||||
public_key = *key;
|
||||
}
|
||||
else if(key == "issuer")
|
||||
else if (key == "issuer")
|
||||
{
|
||||
issuer = std::string(options[index].value);
|
||||
if(issuer.empty())
|
||||
if (issuer.empty())
|
||||
{
|
||||
mosquitto_log_printf(MOSQ_LOG_ERR, "issuer not set.");
|
||||
return MOSQ_ERR_INVAL;
|
||||
@@ -72,50 +76,56 @@ int mosquitto_plugin_init(mosquitto_plugin_id_t *identifier, void **userdata, st
|
||||
authorizer = std::make_unique<Authorizer>(public_key, issuer);
|
||||
|
||||
// Register the callbacks.
|
||||
mosquitto_callback_register(plugin_id, MOSQ_EVT_BASIC_AUTH, j7s_auth_basic_auth_callback, NULL, NULL);
|
||||
mosquitto_callback_register(plugin_id, MOSQ_EVT_ACL_CHECK, j7s_acl_check_callback, NULL, NULL);
|
||||
mosquitto_callback_register(plugin_id, MOSQ_EVT_DISCONNECT, j7s_disconnect_callback, NULL, NULL);
|
||||
mosquitto_callback_register(
|
||||
plugin_id, MOSQ_EVT_BASIC_AUTH, j7s_auth_basic_auth_callback, NULL, NULL);
|
||||
mosquitto_callback_register(plugin_id, MOSQ_EVT_ACL_CHECK, j7s_acl_check_callback, NULL, NULL);
|
||||
mosquitto_callback_register(
|
||||
plugin_id, MOSQ_EVT_DISCONNECT, j7s_disconnect_callback, NULL, NULL);
|
||||
// May want MOSQ_EVT_RELOAD as well.
|
||||
|
||||
return MOSQ_ERR_SUCCESS;
|
||||
}
|
||||
|
||||
int mosquitto_plugin_cleanup(void *userdata, struct mosquitto_opt *options, int option_count)
|
||||
int mosquitto_plugin_cleanup(void * userdata, struct mosquitto_opt * options, int option_count)
|
||||
{
|
||||
if(plugin_id)
|
||||
if (plugin_id)
|
||||
{
|
||||
mosquitto_callback_unregister(plugin_id, MOSQ_EVT_BASIC_AUTH, j7s_auth_basic_auth_callback, NULL);
|
||||
mosquitto_callback_unregister(plugin_id, MOSQ_EVT_ACL_CHECK, j7s_acl_check_callback, NULL);
|
||||
mosquitto_callback_unregister(plugin_id, MOSQ_EVT_DISCONNECT, j7s_disconnect_callback, NULL);
|
||||
mosquitto_callback_unregister(
|
||||
plugin_id, MOSQ_EVT_BASIC_AUTH, j7s_auth_basic_auth_callback, NULL);
|
||||
mosquitto_callback_unregister(plugin_id, MOSQ_EVT_ACL_CHECK, j7s_acl_check_callback, NULL);
|
||||
mosquitto_callback_unregister(
|
||||
plugin_id, MOSQ_EVT_DISCONNECT, j7s_disconnect_callback, NULL);
|
||||
}
|
||||
|
||||
return MOSQ_ERR_SUCCESS;
|
||||
}
|
||||
|
||||
int j7s_auth_basic_auth_callback(int event, void *event_data, void *userdata)
|
||||
int j7s_auth_basic_auth_callback(int event, void * event_data, void * userdata)
|
||||
{
|
||||
if(not authorizer)
|
||||
if (not authorizer)
|
||||
{
|
||||
// Not sure this is possible.
|
||||
return MOSQ_ERR_AUTH;
|
||||
}
|
||||
|
||||
struct mosquitto_evt_basic_auth *auth_data = static_cast<struct mosquitto_evt_basic_auth*>(event_data);
|
||||
struct mosquitto_evt_basic_auth * auth_data =
|
||||
static_cast<struct mosquitto_evt_basic_auth *>(event_data);
|
||||
|
||||
if(!auth_data->username)
|
||||
if (!auth_data->username)
|
||||
{
|
||||
// We need a username to do anything.
|
||||
return MOSQ_ERR_PLUGIN_DEFER;
|
||||
}
|
||||
|
||||
if(!auth_data->password)
|
||||
if (!auth_data->password)
|
||||
{
|
||||
authorizer->add_unknown(std::string(auth_data->username));
|
||||
return MOSQ_ERR_PLUGIN_DEFER;
|
||||
}
|
||||
bool is_authed = authorizer->add(std::string(auth_data->password), std::string(auth_data->username));
|
||||
bool is_authed =
|
||||
authorizer->add(std::string(auth_data->password), std::string(auth_data->username));
|
||||
|
||||
if(is_authed)
|
||||
if (is_authed)
|
||||
{
|
||||
return MOSQ_ERR_SUCCESS;
|
||||
}
|
||||
@@ -125,23 +135,24 @@ int j7s_auth_basic_auth_callback(int event, void *event_data, void *userdata)
|
||||
}
|
||||
}
|
||||
|
||||
int j7s_acl_check_callback(int event, void *event_data, void *userdata)
|
||||
int j7s_acl_check_callback(int event, void * event_data, void * userdata)
|
||||
{
|
||||
if(not authorizer)
|
||||
if (not authorizer)
|
||||
{
|
||||
return MOSQ_ERR_ACL_DENIED;
|
||||
}
|
||||
|
||||
struct mosquitto_evt_acl_check *acl_data = static_cast<struct mosquitto_evt_acl_check *>(event_data);
|
||||
struct mosquitto_evt_acl_check * acl_data =
|
||||
static_cast<struct mosquitto_evt_acl_check *>(event_data);
|
||||
|
||||
const std::string username = std::string(mosquitto_client_username(acl_data->client));
|
||||
|
||||
if(authorizer->is_unknown(username))
|
||||
if (authorizer->is_unknown(username))
|
||||
{
|
||||
return MOSQ_ERR_PLUGIN_DEFER;
|
||||
}
|
||||
|
||||
switch(acl_data->access)
|
||||
switch (acl_data->access)
|
||||
{
|
||||
case MOSQ_ACL_SUBSCRIBE:
|
||||
return MOSQ_ERR_PLUGIN_DEFER;
|
||||
@@ -156,13 +167,12 @@ int j7s_acl_check_callback(int event, void *event_data, void *userdata)
|
||||
}
|
||||
}
|
||||
|
||||
int j7s_disconnect_callback(int event, void *event_data, void *userdata)
|
||||
int j7s_disconnect_callback(int event, void * event_data, void * userdata)
|
||||
{
|
||||
struct mosquitto_evt_disconnect *disconnect_data = static_cast<struct mosquitto_evt_disconnect*>(event_data);
|
||||
struct mosquitto_evt_disconnect * disconnect_data =
|
||||
static_cast<struct mosquitto_evt_disconnect *>(event_data);
|
||||
const std::string username = std::string(mosquitto_client_username(disconnect_data->client));
|
||||
|
||||
authorizer->logout(username);
|
||||
return MOSQ_ERR_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
// Copyright 2022 James Pace
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include <j7s-plugin/utils.h>
|
||||
#include <jwt-cpp/jwt.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
std::optional<std::string> read_key(const std::string & key_file)
|
||||
{
|
||||
// Read key from file.
|
||||
std::ifstream key_stream(key_file, std::ios::binary);
|
||||
if (not key_stream)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
std::stringstream ss;
|
||||
ss << key_stream.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::tuple<bool, bool> validate(
|
||||
const std::string & token,
|
||||
const std::string & username,
|
||||
const std::string & issuer,
|
||||
const std::string & pub_key)
|
||||
{
|
||||
const auto decoded_token = jwt::decode(token);
|
||||
|
||||
// Is the token valid?
|
||||
const auto verifier =
|
||||
jwt::verify().with_issuer(issuer).allow_algorithm(jwt::algorithm::rs256(pub_key));
|
||||
try
|
||||
{
|
||||
verifier.verify(decoded_token);
|
||||
}
|
||||
catch (jwt::error::token_verification_exception & exception)
|
||||
{
|
||||
std::cerr << exception.what() << std::endl;
|
||||
return std::make_tuple(false, false);
|
||||
}
|
||||
auto claims = decoded_token.get_payload_claims();
|
||||
|
||||
// Check username matches.
|
||||
if (not claims.contains("upn"))
|
||||
{
|
||||
std::cerr << "Missing upn." << std::endl;
|
||||
return std::make_tuple(false, false);
|
||||
}
|
||||
if (claims["upn"].as_string() != username)
|
||||
{
|
||||
std::cerr << "Wrong username." << std::endl;
|
||||
return std::make_tuple(false, false);
|
||||
}
|
||||
|
||||
// Check for mqtt-write claim value.
|
||||
if (not(claims.contains("mqtt-write") and claims.contains("mqtt-read")))
|
||||
{
|
||||
std::cerr << "Missing mqtt-write or mqtt-read." << std::endl;
|
||||
return std::make_tuple(false, false);
|
||||
}
|
||||
|
||||
bool can_read = claims["mqtt-read"].as_bool();
|
||||
bool can_write = claims["mqtt-write"].as_bool();
|
||||
|
||||
return std::make_tuple(can_read, can_write);
|
||||
}
|
||||
|
||||
std::string gen_token(
|
||||
const std::string & issuer,
|
||||
const std::string & username,
|
||||
const std::string & can_read,
|
||||
const std::string & can_write,
|
||||
const std::string & pub_key,
|
||||
const std::string & priv_key,
|
||||
const std::chrono::time_point<std::chrono::system_clock> & expr_time)
|
||||
{
|
||||
const auto token = jwt::create()
|
||||
.set_type("JWT")
|
||||
.set_issuer(issuer)
|
||||
.set_payload_claim("upn", jwt::claim(username))
|
||||
.set_payload_claim("mqtt-read", jwt::claim(can_read))
|
||||
.set_payload_claim("mqtt-write", jwt::claim(can_write))
|
||||
.set_expires_at(expr_time)
|
||||
.sign(jwt::algorithm::rs256(pub_key, priv_key, "", ""));
|
||||
|
||||
return token;
|
||||
}
|
||||
Reference in new issue
Block a user