Module: MailMCP::JwtService

Defined in:
lib/mail_mcp/jwt_service.rb

Defined Under Namespace

Classes: Error

Constant Summary collapse

DEFAULT_EXPIRY =
8 * 3600
DEFAULT_REFRESH_EXPIRY =
30 * 24 * 3600
CRED_KEYS =
%w[
  imap_host imap_port imap_ssl imap_username imap_password
  smtp_host smtp_port smtp_ssl smtp_username smtp_password
].freeze
CODE_EXPIRY =

Authorization code — short-lived JWE carrying creds + PKCE state (stateless PKCE)

300

Class Method Summary collapse

Class Method Details

.decode_client_id(token) ⇒ Object

Raises:



78
79
80
81
82
83
# File 'lib/mail_mcp/jwt_service.rb', line 78

def self.decode_client_id(token)
  payload = decode_jwe(token, verify_exp: false)
  raise Error, "Not a client_id token" unless payload["typ"] == "client_id"

  payload
end

.issue(creds, expires_in: DEFAULT_EXPIRY) ⇒ Object

Access token — JWE (dir/A256GCM), credentials embedded directly in payload



16
17
18
# File 'lib/mail_mcp/jwt_service.rb', line 16

def self.issue(creds, expires_in: DEFAULT_EXPIRY)
  issue_jwe(creds.merge("typ" => "access"), expires_in: expires_in)
end

.issue_client_id(imap_host:, imap_port:, imap_ssl:, smtp_host:, smtp_port:, smtp_ssl:, client_secret:) ⇒ Object

Client ID token — JWE, no expiry, carries imap/smtp config + client_secret



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/mail_mcp/jwt_service.rb', line 62

def self.issue_client_id(imap_host:, imap_port:, imap_ssl:, smtp_host:, smtp_port:, smtp_ssl:, client_secret:)
  payload = JSON.generate(
    iss: ENV.fetch("BASE_URL"),
    aud: ENV.fetch("BASE_URL"),
    typ: "client_id",
    imap_host: imap_host,
    imap_port: imap_port.to_i,
    imap_ssl: imap_ssl,
    smtp_host: smtp_host,
    smtp_port: smtp_port.to_i,
    smtp_ssl: smtp_ssl,
    cs: client_secret
  )
  encrypt_jwe(payload)
end

.issue_code(creds:, code_challenge:, redirect_uri:, client_id:) ⇒ Object

5 minutes



42
43
44
45
46
47
48
49
50
51
52
# File 'lib/mail_mcp/jwt_service.rb', line 42

def self.issue_code(creds:, code_challenge:, redirect_uri:, client_id:)
  issue_jwe(
    creds.merge(
      "typ" => "code",
      "code_challenge" => code_challenge,
      "redirect_uri" => redirect_uri,
      "client_id" => client_id
    ),
    expires_in: CODE_EXPIRY
  )
end

.issue_refresh(creds, expires_in: DEFAULT_REFRESH_EXPIRY) ⇒ Object

Refresh token — JWE, longer-lived, carries same credential payload



28
29
30
# File 'lib/mail_mcp/jwt_service.rb', line 28

def self.issue_refresh(creds, expires_in: DEFAULT_REFRESH_EXPIRY)
  issue_jwe(creds.merge("typ" => "refresh"), expires_in: expires_in)
end

.verify(token) ⇒ Object

Raises:



20
21
22
23
24
25
# File 'lib/mail_mcp/jwt_service.rb', line 20

def self.verify(token)
  payload = decode_jwe(token)
  raise Error, "Not an access token" unless payload["typ"] == "access"

  payload.slice(*CRED_KEYS)
end

.verify_code(token) ⇒ Object

Raises:



54
55
56
57
58
59
# File 'lib/mail_mcp/jwt_service.rb', line 54

def self.verify_code(token)
  payload = decode_jwe(token)
  raise Error, "Not an authorization code" unless payload["typ"] == "code"

  payload
end

.verify_refresh(token) ⇒ Object

Raises:



32
33
34
35
36
37
# File 'lib/mail_mcp/jwt_service.rb', line 32

def self.verify_refresh(token)
  payload = decode_jwe(token)
  raise Error, "Not a refresh token" unless payload["typ"] == "refresh"

  payload.slice(*CRED_KEYS)
end