move auth and jwt modules to herolib

This commit is contained in:
timurgordon
2025-01-23 00:02:48 +00:00
parent cab7a47050
commit a95ce8abb2
11 changed files with 1009 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
# Email authentication module
Module to verify user email by sending the user a link.The functions in the module can be implemented manually in a web server, but the recommended way is simply to use the API.
## API
## Examples
- see publisher/view/auth_controllers

View File

@@ -0,0 +1,245 @@
module authentication
import time
import net.smtp
import crypto.hmac
import crypto.sha256
import crypto.rand
import encoding.hex
import encoding.base64
import log
// Creates and updates, authenticates email authentication sessions
@[noinit]
pub struct Authenticator {
secret string
mut:
config SmtpConfig @[required]
backend IBackend // Backend for authenticator
}
// Is initialized when an auth link is sent
// Represents the state of the authentication session
pub struct AuthSession {
pub mut:
email string
timeout time.Time
auth_code string // hex representation of 64 bytes
attempts_left int = 3
authenticated bool
}
@[params]
pub struct AuthenticatorConfig {
secret string
smtp SmtpConfig
backend IBackend
}
pub fn new(config AuthenticatorConfig) !Authenticator {
// send email with link in body
// mut client := smtp.new_client(
// server: config.smtp.server
// from: config.smtp.from
// port: config.smtp.port
// username: config.smtp.username
// password: config.smtp.password
// )!
return Authenticator{
config: config.smtp
// client: smtp.new_client(
// server: config.smtp.server
// from: config.smtp.from
// port: config.smtp.port
// username: config.smtp.username
// password: config.smtp.password
// )!
backend: config.backend
secret: config.secret
}
}
@[params]
pub struct SendMailConfig {
email string
mail VerificationMail
link string
}
pub struct VerificationMail {
pub:
from string = 'email_authenticator@spiderlib.ff'
subject string = 'Verify your email'
body string = 'Please verify your email by clicking the link below'
}
pub struct SmtpConfig {
server string
from string
port int
username string
password string
}
pub fn (mut auth Authenticator) email_authentication(config SendMailConfig) ! {
auth.send_verification_mail(config)!
auth.await_authentication(email: config.email)!
}
// sends mail with verification link
pub fn (mut auth Authenticator) send_verification_mail(config SendMailConfig) ! {
// create auth session
auth_code := rand.bytes(64) or { panic(err) }
auth.backend.create_auth_session(
email: config.email
auth_code: auth_code.hex()
timeout: time.now().add_seconds(180)
)!
link := '<a href="${config.link}/${config.email}/${auth_code.hex()}">Click to authenticate</a>'
mail := smtp.Mail{
to: config.email
from: config.mail.from
subject: config.mail.subject
body_type: .html
body: '${config.mail.body}\n${link}'
}
mut client := smtp.new_client(
server: auth.config.server
from: auth.config.from
port: auth.config.port
username: auth.config.username
password: auth.config.password
)!
client.send(mail) or { return error('Error resolving email address') }
client.quit() or { return error('Could not close connection to server') }
}
// sends mail with login link
pub fn (mut auth Authenticator) send_login_link(config SendMailConfig) ! {
expiration := time.now().add(5 * time.minute)
data := '${config.email}.${expiration}' // data to be signed
signature := hmac.new(hex.decode(auth.secret) or { panic(err) }, data.bytes(), sha256.sum,
sha256.block_size)
encoded_signature := base64.url_encode(signature.bytestr().bytes())
link := '<a href="${config.link}/${config.email}/${expiration.unix()}/${encoded_signature}">Click to login</a>'
mail := smtp.Mail{
to: config.email
from: config.mail.from
subject: config.mail.subject
body_type: .html
body: '${config.mail.body}\n${link}'
}
mut client := smtp.new_client(
server: auth.config.server
from: auth.config.from
port: auth.config.port
username: auth.config.username
password: auth.config.password
)!
client.send(mail) or { panic('Error resolving email address') }
client.quit() or { panic('Could not close connection to server') }
}
pub struct LoginAttempt {
pub:
email string
expiration time.Time
signature string
}
// sends mail with login link
pub fn (mut auth Authenticator) authenticate_login_attempt(attempt LoginAttempt) ! {
if time.now() > attempt.expiration {
return error('link expired')
}
data := '${attempt.email}.${attempt.expiration}' // data to be signed
signature_mirror := hmac.new(hex.decode(auth.secret) or { panic(err) }, data.bytes(),
sha256.sum, sha256.block_size).bytestr().bytes()
decoded_signature := base64.url_decode(attempt.signature)
if !hmac.equal(decoded_signature, signature_mirror) {
return error('signature mismatch')
}
}
// result of an authentication attempt
// returns time and attempts remaining
pub struct AttemptResult {
pub:
authenticated bool
attempts_left int
time_left time.Time
}
enum AuthErrorReason {
cypher_mismatch
no_remaining_attempts
session_not_found
}
struct AuthError {
Error
reason AuthErrorReason
}
// authenticates if email/cypher combo correct within timeout and remaining attemts
// TODO: address based request limits recognition to prevent brute
// TODO: max allowed request per seccond to prevent dos
pub fn (mut auth Authenticator) authenticate(email string, cypher string) ! {
session := auth.backend.read_auth_session(email) or {
return AuthError{
reason: .session_not_found
}
}
if session.attempts_left <= 0 { // checks if remaining attempts
return AuthError{
reason: .no_remaining_attempts
}
}
// authenticates if cypher in link matches authcode
if cypher == session.auth_code {
auth.backend.set_session_authenticated(email) or { panic(err) }
} else {
updated_session := AuthSession{
...session
attempts_left: session.attempts_left - 1
}
auth.backend.update_auth_session(updated_session)!
return AuthError{
reason: .cypher_mismatch
}
}
}
pub struct AwaitAuthParams {
email string @[required]
timeout time.Duration = 3 * time.minute
}
// function to check if an email is authenticated
pub fn (mut auth Authenticator) await_authentication(params AwaitAuthParams) ! {
stopwatch := time.new_stopwatch()
for stopwatch.elapsed() < params.timeout {
if auth.is_authenticated(params.email)! {
return
}
time.sleep(2 * time.second)
}
return error('Authentication timeout.')
}
// function to check if an email is authenticated
pub fn (mut auth Authenticator) is_authenticated(email string) !bool {
session := auth.backend.read_auth_session(email) or { return error('Cant find session') }
return session.authenticated
}

View File

@@ -0,0 +1,14 @@
module authentication
import log
// Creates and updates, authenticates email authentication sessions
interface IBackend {
read_auth_session(string) ?AuthSession
mut:
logger &log.Logger
create_auth_session(AuthSession) !
update_auth_session(AuthSession) !
delete_auth_session(string) !
set_session_authenticated(string) !
}

View File

@@ -0,0 +1,93 @@
module authentication
import db.sqlite
import log
import time
// Creates and updates, authenticates email authentication sessions
@[noinit]
struct DatabaseBackend {
mut:
db sqlite.DB
}
@[params]
pub struct DatabaseBackendConfig {
db_path string = 'email_authenticator.sqlite'
}
// factory for
pub fn new_database_backend(config DatabaseBackendConfig) !DatabaseBackend {
db := sqlite.connect(config.db_path) or { panic(err) }
sql db {
create table AuthSession
} or { panic(err) }
return DatabaseBackend{
// logger: config.logger
db: db
}
}
pub fn (auth DatabaseBackend) create_auth_session(session_ AuthSession) ! {
mut session := session_
if session.timeout.unix() == 0 {
session.timeout = time.now().add_seconds(180)
}
sql auth.db {
insert session into AuthSession
} or { panic('err:${err}') }
}
pub fn (auth DatabaseBackend) read_auth_session(email string) ?AuthSession {
session := sql auth.db {
select from AuthSession where email == '${email}'
} or { panic('err:${err}') }
return session[0] or { return none }
}
pub fn (auth DatabaseBackend) update_auth_session(session AuthSession) ! {
sql auth.db {
update AuthSession set attempts_left = session.attempts_left where email == session.email
} or { panic('err:${err}') }
}
pub fn (auth DatabaseBackend) set_session_authenticated(email string) ! {
sql auth.db {
update AuthSession set authenticated = true where email == email
} or { panic('err:${err}') }
}
pub fn (auth DatabaseBackend) delete_auth_session(email string) ! {
sql auth.db {
delete from AuthSession where email == '${email}'
} or { panic('err:${err}') }
}
// if session.attempts_left <= 0 { // checks if remaining attempts
// return AttemptResult{
// authenticated: false
// attempts_left: 0
// time_left:
// }
// }
// // authenticates if cypher in link matches authcode
// if cypher == auth.sessions[email].auth_code {
// auth.logger.debug(@FN + ':\nUser authenticated email: ${email}')
// auth.sessions[email].authenticated = true
// result := AttemptResult{
// authenticated: true
// attempts_left: auth.sessions[email].attempts_left
// }
// return result
// } else {
// auth.sessions[email].attempts_left -= 1
// result := AttemptResult{
// authenticated: false
// attempts_left: auth.sessions[email].attempts_left
// }
// return result
// }

View File

@@ -0,0 +1,59 @@
module authentication
import db.sqlite
import log
import time
const test_email = 'test@example.com'
const test_auth_code = '123ABC'
const test_db_name = 'email_authenticator.sqlite'
fn testsuite_begin() {
db := sqlite.connect(email.test_db_name) or { panic(err) }
sql db {
drop table AuthSession
} or { return }
}
fn testsuite_end() {
db := sqlite.connect(email.test_db_name) or { panic(err) }
sql db {
drop table AuthSession
} or { panic(err) }
}
fn test_database_backend() {
mut backend := new_database_backend()!
run_backend_tests(mut backend)!
backend.db.close()!
}
fn test_memory_backend() {
mut backend := new_memory_backend()!
run_backend_tests(mut backend)!
}
fn run_backend_tests(mut backend IBackend) ! {
session := AuthSession{
email: email.test_email
}
backend.create_auth_session(session)!
assert backend.read_auth_session(email.test_email)! == session
backend.update_auth_session(AuthSession{
...session
attempts_left: 1
})!
assert backend.read_auth_session(email.test_email)!.attempts_left == 1
backend.delete_auth_session(email.test_email)!
if _ := backend.read_auth_session(email.test_email) {
// should return none, so fails test
assert false
} else {
assert true
}
}

View File

@@ -0,0 +1,68 @@
module authentication
import net.http
import time
import json
// session controller that be be added to vweb projects
pub struct EmailClient {
url string @[required]
}
struct PostParams {
url string
data string
timeout time.Duration
}
fn (client EmailClient) post_request(params PostParams) !http.Response {
mut req := http.new_request(http.Method.post, params.url, params.data)
req.read_timeout = params.timeout
resp := req.do() or {
return error('Failed to send request to email authentication server: ${err.code}')
}
if resp.status_code == 404 {
return error('Could not find email verification endpoint, please make sure the auth client url is configured to the url the auth server is running at.')
}
if resp.status_code != 200 {
panic('Email verification request failed, this should never happen: ${resp.status_msg}')
}
return resp
}
// verify_email posts an email verification req to the email auth controller
pub fn (client EmailClient) email_authentication(params SendMailConfig) ! {
client.post_request(
url: '${client.url}/email_authentication'
data: json.encode(params)
timeout: 180 * time.second
)!
}
// verify_email posts an email verification req to the email auth controller
pub fn (client EmailClient) is_verified(address string) !bool {
resp := client.post_request(
url: '${client.url}/is_verified'
data: json.encode(address)
timeout: 180 * time.second
)!
return resp.body == 'true'
}
// send_verification_email posts an email verification req to the email auth controller
pub fn (client EmailClient) send_verification_email(params SendMailConfig) ! {
client.post_request(
url: '${client.url}/send_verification_mail'
data: json.encode(params)
) or { return error(err.msg()) }
}
// authenticate posts an authentication attempt req to the email auth controller
pub fn (c EmailClient) authenticate(address string, cypher string) !AttemptResult {
resp := http.post('${c.url}/authenticate', json.encode(AuthAttempt{
address: address
cypher: cypher
}))!
result := json.decode(AttemptResult, resp.body)!
return result
}

View File

@@ -0,0 +1,145 @@
module authentication
import vweb
import time
import json
import log
import freeflowuniverse.herolib.ui.console
const agent = 'Email Authentication Controller'
// email authentication controller that be be added to vweb projects
@[heap]
pub struct Controller {
vweb.Context
callback string @[vweb_global]
mut:
authenticator Authenticator @[vweb_global]
}
@[params]
pub struct ControllerParams {
logger &log.Logger
authenticator Authenticator @[required]
}
pub fn new_controller(params ControllerParams) Controller {
mut app := Controller{
authenticator: params.authenticator
}
return app
}
// route responsible for verifying email, email form should be posted here
@[POST]
pub fn (mut app Controller) send_verification_mail() !vweb.Result {
config := json.decode(SendMailConfig, app.req.data)!
app.authenticator.send_verification_mail(config) or { panic(err) }
return app.ok('')
}
// route responsible for verifying email, email form should be posted here
@[POST]
pub fn (mut app Controller) is_verified() vweb.Result {
address := app.req.data
// checks if email verified every 2 seconds
for {
if app.authenticator.is_authenticated(address) or { panic(err) } {
// returns success message once verified
return app.ok('ok')
}
time.sleep(2 * time.second)
}
return app.html('timeout')
}
// route responsible for verifying email, email form should be posted here
@[POST]
pub fn (mut app Controller) email_authentication() vweb.Result {
config_ := json.decode(SendMailConfig, app.req.data) or {
app.set_status(422, 'Request payload does not follow anticipated formatting.')
return app.text('Request payload does not follow anticipated formatting.')
}
config := if config_.link == '' {
SendMailConfig{
...config_
link: 'http://localhost:8000/email_authenticator/authentication_link'
}
} else {
config_
}
app.authenticator.send_verification_mail(config) or { panic(err) }
// checks if email verified every 2 seconds
for {
if app.authenticator.is_authenticated(config.email) or { panic(err) } {
// returns success message once verified
return app.ok('ok')
}
time.sleep(2 * time.second)
}
return app.ok('success!')
}
// route responsible for verifying email, email form should be posted here
@[POST]
pub fn (mut app Controller) verify() vweb.Result {
config_ := json.decode(SendMailConfig, app.req.data) or {
app.set_status(422, 'Request payload does not follow anticipated formatting.')
return app.text('Request payload does not follow anticipated formatting.')
}
config := if config_.link == '' {
SendMailConfig{
...config_
link: 'http://localhost:8000/email_authenticator/authentication_link'
}
} else {
config_
}
app.authenticator.send_verification_mail(config) or { panic(err) }
// checks if email verified every 2 seconds
stopwatch := time.new_stopwatch()
for stopwatch.elapsed() < 180 * time.second {
authenticated := app.authenticator.is_authenticated(config.email) or {
return app.text(err.msg())
}
if authenticated {
console.print_debug('heyo yess')
return app.ok('success')
}
time.sleep(2 * time.second)
}
app.set_status(408, 'Email authentication timeout.')
return app.text('Email authentication timeout.')
}
pub struct AuthAttempt {
pub:
ip string
address string
cypher string
}
@[POST]
pub fn (mut app Controller) authenticate() !vweb.Result {
attempt := json.decode(AuthAttempt, app.req.data)!
app.authenticator.authenticate(attempt.address, attempt.cypher) or {
app.set_status(401, err.msg())
return app.text('Failed to authenticate')
}
return app.ok('Authentication successful')
}
@['/authentication_link/:address/:cypher']
pub fn (mut app Controller) authentication_link(address string, cypher string) !vweb.Result {
app.authenticator.authenticate(address, cypher) or {
app.set_status(401, err.msg())
return app.text('Failed to authenticate')
}
return app.html('Authentication successful')
}

View File

@@ -0,0 +1,46 @@
module authentication
import log
import net.smtp
import os
import toml
fn test_new_controller() {
mut logger := log.Logger(&log.Log{
level: .debug
})
env := toml.parse_file(os.dir(os.dir(@FILE)) + '/.env') or {
panic('Could not find .env, ${err}')
}
client := smtp.Client{
server: 'smtp-relay.brevo.com'
from: 'verify@authenticator.io'
port: 587
username: env.value('BREVO_SMTP_USERNAME').string()
password: env.value('BREVO_SMTP_PASSWORD').string()
}
controller := new_controller(logger: &logger)
}
fn test_send_verification_mail() {
// mut logger := log.Logger(&log.Log{
// level: .debug
// })
// env := toml.parse_file(os.dir(os.dir(@FILE)) + '/.env') or {
// panic('Could not find .env, ${err}')
// }
// client := smtp.Client{
// server: 'smtp-relay.brevo.com'
// from: 'verify@authenticator.io'
// port: 587
// username: env.value('BREVO_SMTP_USERNAME').string()
// password: env.value('BREVO_SMTP_PASSWORD').string()
// }
// controller := new_controller(logger: &logger)
}

View File

@@ -0,0 +1,106 @@
module authentication
import time
import crypto.hmac
import crypto.sha256
import encoding.hex
import encoding.base64
import freeflowuniverse.herolib.clients.mailclient {MailClient}
pub struct StatelessAuthenticator {
pub:
secret string
pub mut:
mail_client MailClient
}
pub fn new_stateless_authenticator(authenticator StatelessAuthenticator) !StatelessAuthenticator {
// TODO: do some checks
return StatelessAuthenticator {...authenticator}
}
pub struct AuthenticationMail {
RedirectURLs
pub:
to string // email address being authentcated
from string = 'email_authenticator@herolib.tf'
subject string = 'Verify your email'
body string = 'Please verify your email by clicking the link below'
callback string // callback url of authentication link
success_url string // where the user will be redirected upon successful authentication
failure_url string // where the user will be redirected upon failed authentication
}
pub fn (mut a StatelessAuthenticator) send_authentication_mail(mail AuthenticationMail) ! {
link := a.new_authentication_link(mail.to, mail.callback, mail.RedirectURLs)!
button := '<a href="${link}" style="display:inline-block; padding:10px 20px; font-size:16px; color:white; background-color:#4CAF50; text-decoration:none; border-radius:5px;">Verify Email</a>'
// send email with link in body
a.mail_client.send(
to: mail.to
from: mail.from
subject: mail.subject
body_type: .html
body: $tmpl('./templates/mail.html')
) or { return error('Error resolving email address $err') }
}
@[params]
pub struct RedirectURLs {
pub:
success_url string
failure_url string
}
fn (a StatelessAuthenticator) new_authentication_link(email string, callback string, urls RedirectURLs) !string {
if urls.failure_url != '' {
panic('implement')
}
// sign email address and expiration of authentication link
expiration := time.now().add(5 * time.minute)
data := '${email}.${expiration}' // data to be signed
// QUESTION? should success url also be signed for security?
signature := hmac.new(
hex.decode(a.secret)!,
data.bytes(),
sha256.sum,
sha256.block_size
)
encoded_signature := base64.url_encode(signature.bytestr().bytes())
mut queries := ''
if urls.success_url != '' {
encoded_url := base64.url_encode(urls.success_url.bytes())
queries += '?success_url=${encoded_url}'
}
return "${callback}/${email}/${expiration.unix()}/${encoded_signature}${queries}"
}
pub struct AuthenticationAttempt {
pub:
email string
expiration time.Time
signature string
}
// sends mail with login link
pub fn (auth StatelessAuthenticator) authenticate(attempt AuthenticationAttempt) ! {
if time.now() > attempt.expiration {
return error('link expired')
}
data := '${attempt.email}.${attempt.expiration}' // data to be signed
signature_mirror := hmac.new(
hex.decode(auth.secret) or {panic(err)},
data.bytes(),
sha256.sum,
sha256.block_size
).bytestr().bytes()
decoded_signature := base64.url_decode(attempt.signature)
if !hmac.equal(decoded_signature, signature_mirror) {
return error('signature mismatch')
}
}

View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: Arial, sans-serif;
color: #333333;
line-height: 1.6;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
border: 1px solid #dddddd;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
font-size: 24px;
margin-bottom: 20px;
color: #4CAF50;
}
.content {
margin-bottom: 20px;
}
.footer {
font-size: 12px;
color: #888888;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">Verify Your Email</div>
<div class="content">
<p>Hello,</p>
<p>@{mail.body}</p>
<p>@{button}</p>
</div>
<div class="footer">
<p>If you did not request this email, please ignore it.</p>
<p>Thank you,<br>The OurWorld Team</p>
</div>
</div>
</body>
</html>

175
lib/security/jwt/jwt.v Normal file
View File

@@ -0,0 +1,175 @@
module jwt
import crypto.hmac
import crypto.sha256
import encoding.base64
import json
import x.json2
import time
import crypto.rand
import os
// JWT code in this page is from
// https://github.com/vlang/v/blob/master/examples/vweb_orm_jwt/src/auth_services.v
// credit to https://github.com/enghitalo
pub struct JsonWebToken {
JwtHeader
JwtPayload
}
struct JwtHeader {
alg string
typ string
}
// TODO: refactor to use single JWT interface
// todo: we can name these better
pub struct JwtPayload {
pub:
sub string // (subject)
iss string // (issuer)
exp time.Time // (expiration)
iat time.Time // (issued at)
aud string // (audience)
data string
}
// creates jwt with encoded payload and header
// DOESN'T handle data encryption, sensitive data should be encrypted
pub fn create_token(payload_ JwtPayload) JsonWebToken {
return JsonWebToken{
JwtHeader: JwtHeader{'HS256', 'JWT'}
JwtPayload: JwtPayload{
...payload_
iat: time.now()
}
}
}
pub fn create_secret() string {
bytes := rand.bytes(64) or { panic('Creating JWT Secret: ${err}') }
return bytes.bytestr()
}
pub fn (token JsonWebToken) sign(secret string) string {
header := base64.url_encode(json.encode(token.JwtHeader).bytes())
payload := base64.url_encode(json.encode(token.JwtPayload).bytes())
signature := base64.url_encode(hmac.new(secret.bytes(), '${header}.${payload}'.bytes(),
sha256.sum, sha256.block_size).bytestr().bytes())
return '${header}.${payload}.${signature}'
}
pub fn (token JsonWebToken) is_expired() bool {
return token.exp <= time.now()
}
pub type SignedJWT = string
pub fn (token SignedJWT) is_valid() bool {
return token.count('.') == 2
}
pub fn (token SignedJWT) verify(secret string) !bool {
if !token.is_valid() {
return error('Token `${token}` is not valid')
}
signature_mirror := hmac.new(secret.bytes(), token.all_before_last('.').bytes(), sha256.sum,
sha256.block_size).bytestr().bytes()
signature_token := base64.url_decode(token.all_after_last('.'))
return hmac.equal(signature_token, signature_mirror)
}
// gets cookie token, returns user obj
pub fn (token SignedJWT) decode() !JsonWebToken {
if !token.is_valid() {
return error('Token `${token}` is not valid')
}
header_urlencoded := token.split('.')[0]
header_json := base64.url_decode(header_urlencoded).bytestr()
header := json.decode(JwtHeader, header_json) or { panic('Decode header: ${err}') }
payload_urlencoded := token.split('.')[1]
payload_json := base64.url_decode(payload_urlencoded).bytestr()
payload := json.decode(JwtPayload, payload_json) or { panic('Decoding payload: ${err}') }
return JsonWebToken{
JwtHeader: header
JwtPayload: payload
}
}
// gets cookie token, returns user obj
pub fn (token SignedJWT) get_field(field string) !string {
if !token.is_valid() {
return error('Token `${token}` is not valid')
}
header_urlencoded := token.split('.')[0]
header_json := base64.url_decode(header_urlencoded).bytestr()
header := json.decode(JwtHeader, header_json) or { panic('Decode header: ${err}') }
payload_urlencoded := token.split('.')[1]
payload_json := base64.url_decode(payload_urlencoded).bytestr()
payload_raw := json2.raw_decode(payload_json) or { panic('Decoding payload: ${err}') }
payload_map := payload_raw.as_map()
return payload_map[field].str()
}
// gets cookie token, returns user obj
pub fn (token SignedJWT) decode_subject() !string {
decoded := token.decode()!
return decoded.sub
}
// verifies jwt cookie
pub fn verify_jwt(token string) bool {
if token == '' {
return false
}
secret := os.getenv('SECRET_KEY')
token_split := token.split('.')
signature_mirror := hmac.new(secret.bytes(), '${token_split[0]}.${token_split[1]}'.bytes(),
sha256.sum, sha256.block_size).bytestr().bytes()
signature_from_token := base64.url_decode(token_split[2])
return hmac.equal(signature_from_token, signature_mirror)
}
// verifies jwt cookie
// todo: implement assymetric verification
pub fn verify_jwt_assymetric(token string, pk string) bool {
return false
}
// gets cookie token, returns user obj
pub fn get_data(token string) !string {
if token == '' {
return error('Failed to decode token: token is empty')
}
payload := json.decode(JwtPayload, base64.url_decode(token.split('.')[1]).bytestr()) or {
panic(err)
}
return payload.data
}
// gets cookie token, returns user obj
pub fn get_payload(token string) !JwtPayload {
if token == '' {
return error('Failed to decode token: token is empty')
}
encoded_payload := base64.url_decode(token.split('.')[1]).bytestr()
return json.decode(JwtPayload, encoded_payload)!
}
// // gets cookie token, returns access obj
// pub fn get_access(token string, username string) ?Access {
// if token == '' {
// return error('Cookie token is empty')
// }
// payload := json.decode(AccessPayload, base64.url_decode(token.split('.')[1]).bytestr()) or {
// panic(err)
// }
// if payload.user != username {
// return error('Access cookie is for different user')
// }
// return payload.access
// }