diff --git a/lib/security/authentication/README.md b/lib/security/authentication/README.md new file mode 100644 index 00000000..2baab162 --- /dev/null +++ b/lib/security/authentication/README.md @@ -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 diff --git a/lib/security/authentication/authenticator.v b/lib/security/authentication/authenticator.v new file mode 100644 index 00000000..f6f4335b --- /dev/null +++ b/lib/security/authentication/authenticator.v @@ -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 := 'Click to authenticate' + 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 := 'Click to login' + 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 +} diff --git a/lib/security/authentication/backend.v b/lib/security/authentication/backend.v new file mode 100644 index 00000000..77209b33 --- /dev/null +++ b/lib/security/authentication/backend.v @@ -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) ! +} diff --git a/lib/security/authentication/backend_database.v b/lib/security/authentication/backend_database.v new file mode 100644 index 00000000..b133b7dc --- /dev/null +++ b/lib/security/authentication/backend_database.v @@ -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 +// } diff --git a/lib/security/authentication/backend_test.v b/lib/security/authentication/backend_test.v new file mode 100644 index 00000000..3047312f --- /dev/null +++ b/lib/security/authentication/backend_test.v @@ -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 + } +} diff --git a/lib/security/authentication/client.v b/lib/security/authentication/client.v new file mode 100644 index 00000000..2e38f2a4 --- /dev/null +++ b/lib/security/authentication/client.v @@ -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 +} diff --git a/lib/security/authentication/controller.v b/lib/security/authentication/controller.v new file mode 100644 index 00000000..9f24d7e5 --- /dev/null +++ b/lib/security/authentication/controller.v @@ -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') +} diff --git a/lib/security/authentication/controller_test.v b/lib/security/authentication/controller_test.v new file mode 100644 index 00000000..4394274a --- /dev/null +++ b/lib/security/authentication/controller_test.v @@ -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) +} diff --git a/lib/security/authentication/email_authentication.v b/lib/security/authentication/email_authentication.v new file mode 100644 index 00000000..0544d996 --- /dev/null +++ b/lib/security/authentication/email_authentication.v @@ -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 := 'Verify Email' + + // 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') + } +} diff --git a/lib/security/authentication/templates/mail.html b/lib/security/authentication/templates/mail.html new file mode 100644 index 00000000..ff3f5cb1 --- /dev/null +++ b/lib/security/authentication/templates/mail.html @@ -0,0 +1,49 @@ + + + + + + + + +
+
Verify Your Email
+
+

Hello,

+

@{mail.body}

+

@{button}

+
+ +
+ + \ No newline at end of file diff --git a/lib/security/jwt/jwt.v b/lib/security/jwt/jwt.v new file mode 100644 index 00000000..9f70712b --- /dev/null +++ b/lib/security/jwt/jwt.v @@ -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 +// }