mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
274 lines
7.2 KiB
Go
274 lines
7.2 KiB
Go
//go:build js && wasm
|
|||
|
|
// +build js,wasm
|
||
|
|
|
||
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"crypto"
|
||
|
|
"crypto/rand"
|
||
|
|
"crypto/rsa"
|
||
|
|
"crypto/sha256"
|
||
|
|
"crypto/x509"
|
||
|
|
"encoding/base64"
|
||
|
|
"encoding/json"
|
||
|
|
"encoding/pem"
|
||
|
|
"fmt"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// JWTManager handles JWT token operations
|
||
|
|
type JWTManager struct {
|
||
|
|
privateKey *rsa.PrivateKey
|
||
|
|
publicKey *rsa.PublicKey
|
||
|
|
kid string
|
||
|
|
issuer string
|
||
|
|
}
|
||
|
|
|
||
|
|
// JWTHeader represents JWT header
|
||
|
|
type JWTHeader struct {
|
||
|
|
Alg string `json:"alg"`
|
||
|
|
Typ string `json:"typ"`
|
||
|
|
Kid string `json:"kid,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// JWTClaims represents standard JWT claims
|
||
|
|
type JWTClaims struct {
|
||
|
|
Issuer string `json:"iss,omitempty"`
|
||
|
|
Subject string `json:"sub,omitempty"`
|
||
|
|
Audience interface{} `json:"aud,omitempty"` // Can be string or []string
|
||
|
|
Expiration int64 `json:"exp,omitempty"`
|
||
|
|
NotBefore int64 `json:"nbf,omitempty"`
|
||
|
|
IssuedAt int64 `json:"iat,omitempty"`
|
||
|
|
JWTID string `json:"jti,omitempty"`
|
||
|
|
Nonce string `json:"nonce,omitempty"`
|
||
|
|
Extra map[string]interface{} `json:"-"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// IDToken represents an OpenID Connect ID token
|
||
|
|
type IDToken struct {
|
||
|
|
JWTClaims
|
||
|
|
AuthTime int64 `json:"auth_time,omitempty"`
|
||
|
|
Nonce string `json:"nonce,omitempty"`
|
||
|
|
ACR string `json:"acr,omitempty"`
|
||
|
|
AMR []string `json:"amr,omitempty"`
|
||
|
|
AZP string `json:"azp,omitempty"`
|
||
|
|
Name string `json:"name,omitempty"`
|
||
|
|
GivenName string `json:"given_name,omitempty"`
|
||
|
|
FamilyName string `json:"family_name,omitempty"`
|
||
|
|
Email string `json:"email,omitempty"`
|
||
|
|
EmailVerified bool `json:"email_verified,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// Global JWT manager instance
|
||
|
|
var jwtManager *JWTManager
|
||
|
|
|
||
|
|
// InitJWTManager initializes the JWT manager
|
||
|
|
func InitJWTManager() error {
|
||
|
|
// Generate RSA key pair
|
||
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("failed to generate RSA key: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
jwtManager = &JWTManager{
|
||
|
|
privateKey: privateKey,
|
||
|
|
publicKey: &privateKey.PublicKey,
|
||
|
|
kid: "motor-key-1",
|
||
|
|
issuer: "https://motor.sonr.io",
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GenerateToken generates a JWT token
|
||
|
|
func (m *JWTManager) GenerateToken(claims JWTClaims) (string, error) {
|
||
|
|
// Set standard claims
|
||
|
|
if claims.Issuer == "" {
|
||
|
|
claims.Issuer = m.issuer
|
||
|
|
}
|
||
|
|
if claims.IssuedAt == 0 {
|
||
|
|
claims.IssuedAt = time.Now().Unix()
|
||
|
|
}
|
||
|
|
if claims.Expiration == 0 {
|
||
|
|
claims.Expiration = time.Now().Add(1 * time.Hour).Unix()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create header
|
||
|
|
header := JWTHeader{
|
||
|
|
Alg: "RS256",
|
||
|
|
Typ: "JWT",
|
||
|
|
Kid: m.kid,
|
||
|
|
}
|
||
|
|
|
||
|
|
// Encode header
|
||
|
|
headerJSON, _ := json.Marshal(header)
|
||
|
|
headerEncoded := base64.RawURLEncoding.EncodeToString(headerJSON)
|
||
|
|
|
||
|
|
// Encode claims
|
||
|
|
claimsJSON, _ := json.Marshal(claims)
|
||
|
|
claimsEncoded := base64.RawURLEncoding.EncodeToString(claimsJSON)
|
||
|
|
|
||
|
|
// Create signature
|
||
|
|
message := headerEncoded + "." + claimsEncoded
|
||
|
|
hash := sha256.Sum256([]byte(message))
|
||
|
|
signature, err := rsa.SignPKCS1v15(rand.Reader, m.privateKey, crypto.SHA256, hash[:])
|
||
|
|
if err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
signatureEncoded := base64.RawURLEncoding.EncodeToString(signature)
|
||
|
|
|
||
|
|
// Combine parts
|
||
|
|
token := message + "." + signatureEncoded
|
||
|
|
return token, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GenerateIDToken generates an OpenID Connect ID token
|
||
|
|
func (m *JWTManager) GenerateIDToken(subject, audience, nonce string, extra map[string]interface{}) (string, error) {
|
||
|
|
idToken := IDToken{
|
||
|
|
JWTClaims: JWTClaims{
|
||
|
|
Issuer: m.issuer,
|
||
|
|
Subject: subject,
|
||
|
|
Audience: audience,
|
||
|
|
IssuedAt: time.Now().Unix(),
|
||
|
|
Expiration: time.Now().Add(1 * time.Hour).Unix(),
|
||
|
|
Nonce: nonce,
|
||
|
|
},
|
||
|
|
AuthTime: time.Now().Unix(),
|
||
|
|
Email: fmt.Sprintf("%s@motor.sonr.io", subject),
|
||
|
|
EmailVerified: true,
|
||
|
|
}
|
||
|
|
|
||
|
|
// Convert to claims
|
||
|
|
claims := JWTClaims{
|
||
|
|
Issuer: idToken.Issuer,
|
||
|
|
Subject: idToken.Subject,
|
||
|
|
Audience: idToken.Audience,
|
||
|
|
IssuedAt: idToken.IssuedAt,
|
||
|
|
Expiration: idToken.Expiration,
|
||
|
|
Nonce: idToken.Nonce,
|
||
|
|
Extra: map[string]interface{}{
|
||
|
|
"auth_time": idToken.AuthTime,
|
||
|
|
"email": idToken.Email,
|
||
|
|
"email_verified": idToken.EmailVerified,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add extra claims
|
||
|
|
for k, v := range extra {
|
||
|
|
claims.Extra[k] = v
|
||
|
|
}
|
||
|
|
|
||
|
|
return m.GenerateToken(claims)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ValidateToken validates a JWT token
|
||
|
|
func (m *JWTManager) ValidateToken(tokenString string) (*JWTClaims, error) {
|
||
|
|
// Split token
|
||
|
|
parts := strings.Split(tokenString, ".")
|
||
|
|
if len(parts) != 3 {
|
||
|
|
return nil, fmt.Errorf("invalid token format")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Decode header
|
||
|
|
headerJSON, err := base64.RawURLEncoding.DecodeString(parts[0])
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to decode header: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
var header JWTHeader
|
||
|
|
if err := json.Unmarshal(headerJSON, &header); err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to parse header: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verify algorithm
|
||
|
|
if header.Alg != "RS256" {
|
||
|
|
return nil, fmt.Errorf("unsupported algorithm: %s", header.Alg)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Decode claims
|
||
|
|
claimsJSON, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to decode claims: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
var claims JWTClaims
|
||
|
|
if err := json.Unmarshal(claimsJSON, &claims); err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to parse claims: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verify signature
|
||
|
|
message := parts[0] + "." + parts[1]
|
||
|
|
signature, err := base64.RawURLEncoding.DecodeString(parts[2])
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to decode signature: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
hash := sha256.Sum256([]byte(message))
|
||
|
|
if err := rsa.VerifyPKCS1v15(m.publicKey, crypto.SHA256, hash[:], signature); err != nil {
|
||
|
|
return nil, fmt.Errorf("invalid signature: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verify expiration
|
||
|
|
if claims.Expiration > 0 && time.Now().Unix() > claims.Expiration {
|
||
|
|
return nil, fmt.Errorf("token expired")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verify not before
|
||
|
|
if claims.NotBefore > 0 && time.Now().Unix() < claims.NotBefore {
|
||
|
|
return nil, fmt.Errorf("token not yet valid")
|
||
|
|
}
|
||
|
|
|
||
|
|
return &claims, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetPublicKeyJWK returns the public key in JWK format
|
||
|
|
func (m *JWTManager) GetPublicKeyJWK() map[string]interface{} {
|
||
|
|
// Get modulus and exponent
|
||
|
|
n := base64.RawURLEncoding.EncodeToString(m.publicKey.N.Bytes())
|
||
|
|
e := base64.RawURLEncoding.EncodeToString([]byte{1, 0, 1}) // 65537
|
||
|
|
|
||
|
|
return map[string]interface{}{
|
||
|
|
"kty": "RSA",
|
||
|
|
"use": "sig",
|
||
|
|
"kid": m.kid,
|
||
|
|
"alg": "RS256",
|
||
|
|
"n": n,
|
||
|
|
"e": e,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetPublicKeyPEM returns the public key in PEM format
|
||
|
|
func (m *JWTManager) GetPublicKeyPEM() string {
|
||
|
|
pubKeyBytes, _ := x509.MarshalPKIXPublicKey(m.publicKey)
|
||
|
|
pubKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||
|
|
Type: "PUBLIC KEY",
|
||
|
|
Bytes: pubKeyBytes,
|
||
|
|
})
|
||
|
|
return string(pubKeyPEM)
|
||
|
|
}
|
||
|
|
|
||
|
|
// GenerateAccessToken generates an access token
|
||
|
|
func (m *JWTManager) GenerateAccessToken(subject, scope string) (string, error) {
|
||
|
|
claims := JWTClaims{
|
||
|
|
Subject: subject,
|
||
|
|
Extra: map[string]interface{}{
|
||
|
|
"scope": scope,
|
||
|
|
"token_type": "Bearer",
|
||
|
|
},
|
||
|
|
}
|
||
|
|
return m.GenerateToken(claims)
|
||
|
|
}
|
||
|
|
|
||
|
|
// GenerateRefreshToken generates a refresh token
|
||
|
|
func (m *JWTManager) GenerateRefreshToken(subject string) (string, error) {
|
||
|
|
claims := JWTClaims{
|
||
|
|
Subject: subject,
|
||
|
|
Expiration: time.Now().Add(30 * 24 * time.Hour).Unix(), // 30 days
|
||
|
|
Extra: map[string]interface{}{
|
||
|
|
"token_type": "refresh",
|
||
|
|
},
|
||
|
|
}
|
||
|
|
return m.GenerateToken(claims)
|
||
|
|
}
|