feat: sync Telegram Login OIDC provider
This commit is contained in:
parent
30774f8c39
commit
ebead9e98c
63 changed files with 11374 additions and 37 deletions
188
internal/app/telegramlogin/jose_test.go
Normal file
188
internal/app/telegramlogin/jose_test.go
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
package telegramlogin
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||
"github.com/lestrrat-go/jwx/v3/jwt"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func telegramLoginTestSigningKeys(t *testing.T, now *time.Time) *SigningKeyRing {
|
||||
t.Helper()
|
||||
oldRSA, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
activeRSA, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
es256, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, ed25519Key, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ring, err := NewSigningKeyRing([]SigningKeyMaterial{
|
||||
{Algorithm: domain.TelegramLoginSigningRS256, KeyID: "rsa-old", PrivateKey: oldRSA, PublishUntil: now.Add(2 * time.Hour)},
|
||||
{Algorithm: domain.TelegramLoginSigningRS256, KeyID: "rsa-active", PrivateKey: activeRSA, Active: true},
|
||||
{Algorithm: domain.TelegramLoginSigningES256, KeyID: "p256-active", PrivateKey: es256, Active: true},
|
||||
{Algorithm: domain.TelegramLoginSigningEdDSA, KeyID: "ed25519-active", PrivateKey: ed25519Key, Active: true},
|
||||
}, func() time.Time { return *now })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return ring
|
||||
}
|
||||
|
||||
func TestSigningKeyRingRotationAndAlgorithms(t *testing.T) {
|
||||
now := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC)
|
||||
ring := telegramLoginTestSigningKeys(t, &now)
|
||||
if got := ring.SupportedAlgorithms(); len(got) != 3 || got[0] != "RS256" || got[1] != "ES256" || got[2] != "EdDSA" {
|
||||
t.Fatalf("SupportedAlgorithms = %#v", got)
|
||||
}
|
||||
body, etag, err := ring.JWKS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
set, err := jwk.Parse(body)
|
||||
if err != nil {
|
||||
t.Fatalf("parse JWKS: %v", err)
|
||||
}
|
||||
if set.Len() != 4 || etag == "" {
|
||||
t.Fatalf("JWKS len=%d etag=%q body=%s", set.Len(), etag, body)
|
||||
}
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal(body, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := 0; i < set.Len(); i++ {
|
||||
key, _ := set.Key(i)
|
||||
if key.Has("d") || key.Has("p") || key.Has("q") {
|
||||
t.Fatalf("JWKS leaked private key material: %s", body)
|
||||
}
|
||||
}
|
||||
now = now.Add(3 * time.Hour)
|
||||
body, _, err = ring.JWKS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
set, err = jwk.Parse(body)
|
||||
if err != nil || set.Len() != 3 {
|
||||
t.Fatalf("JWKS after retirement len=%d err=%v body=%s", set.Len(), err, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIDTokenIssuerScopeProjectionAndVerification(t *testing.T) {
|
||||
now := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC)
|
||||
ring := telegramLoginTestSigningKeys(t, &now)
|
||||
issuer, err := NewIDTokenIssuer(ring, IDTokenIssuerConfig{
|
||||
Issuer: "https://oauth.telesrv.test", Now: func() time.Time { return now },
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
profileRequest := domain.TelegramLoginRequest{
|
||||
ClientID: "9001", SigningAlgorithm: domain.TelegramLoginSigningRS256,
|
||||
Scopes: []domain.TelegramLoginScope{
|
||||
domain.TelegramLoginScopeOpenID, domain.TelegramLoginScopeProfile, domain.TelegramLoginScopePhone,
|
||||
},
|
||||
Nonce: "request-nonce", Status: domain.TelegramLoginRequestApproved, AuthorizedUserID: 42,
|
||||
ProfileName: "Alice Example", GivenName: "Alice", FamilyName: "Example",
|
||||
PreferredUsername: "alice", Picture: "https://oauth.telesrv.test/userpic/42",
|
||||
PhoneNumber: "15551234567", PhoneShared: true, ApprovedAt: now.Add(-time.Minute),
|
||||
}
|
||||
signed, err := issuer.Issue(profileRequest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
jwksBody, _, err := ring.JWKS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
set, err := jwk.Parse(jwksBody)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
token, err := jwt.Parse([]byte(signed), jwt.WithKeySet(set), jwt.WithValidate(false))
|
||||
if err != nil {
|
||||
t.Fatalf("verify signed token: %v", err)
|
||||
}
|
||||
issuerValue, _ := token.Issuer()
|
||||
subject, _ := token.Subject()
|
||||
audience, _ := token.Audience()
|
||||
if issuerValue != "https://oauth.telesrv.test" || subject != "42" || len(audience) != 1 || audience[0] != "9001" {
|
||||
t.Fatalf("standard claims iss=%q sub=%q aud=%#v", issuerValue, subject, audience)
|
||||
}
|
||||
var id float64
|
||||
var name, phone, nonce string
|
||||
var verified bool
|
||||
if err := token.Get("id", &id); err != nil || id != 42 {
|
||||
t.Fatalf("id claim=%v err=%v", id, err)
|
||||
}
|
||||
if err := token.Get("name", &name); err != nil || name != "Alice Example" {
|
||||
t.Fatalf("name claim=%q err=%v", name, err)
|
||||
}
|
||||
if err := token.Get("phone_number", &phone); err != nil || phone != "15551234567" {
|
||||
t.Fatalf("phone claim=%q err=%v", phone, err)
|
||||
}
|
||||
if err := token.Get("phone_number_verified", &verified); err != nil || !verified {
|
||||
t.Fatalf("phone verified=%v err=%v", verified, err)
|
||||
}
|
||||
if err := token.Get("nonce", &nonce); err != nil || nonce != "request-nonce" {
|
||||
t.Fatalf("nonce=%q err=%v", nonce, err)
|
||||
}
|
||||
|
||||
openidOnly := profileRequest
|
||||
openidOnly.SigningAlgorithm = domain.TelegramLoginSigningEdDSA
|
||||
openidOnly.Scopes = []domain.TelegramLoginScope{domain.TelegramLoginScopeOpenID}
|
||||
openidOnly.ProfileName = ""
|
||||
openidOnly.GivenName = ""
|
||||
openidOnly.FamilyName = ""
|
||||
openidOnly.PreferredUsername = ""
|
||||
openidOnly.Picture = ""
|
||||
openidOnly.PhoneNumber = ""
|
||||
openidOnly.PhoneShared = false
|
||||
signed, err = issuer.Issue(openidOnly)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
token, err = jwt.Parse([]byte(signed), jwt.WithKeySet(set), jwt.WithValidate(false))
|
||||
if err != nil {
|
||||
t.Fatalf("verify EdDSA token: %v", err)
|
||||
}
|
||||
if token.Has("id") || token.Has("name") || token.Has("phone_number") {
|
||||
t.Fatalf("openid-only token leaked optional claims: %#v", token.Keys())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSigningKeyRingRejectsWrongCurveAndDuplicateActiveKey(t *testing.T) {
|
||||
p384, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := NewSigningKeyRing([]SigningKeyMaterial{{
|
||||
Algorithm: domain.TelegramLoginSigningES256, PrivateKey: p384, Active: true,
|
||||
}}, nil); err == nil {
|
||||
t.Fatal("P-384 key unexpectedly accepted for ES256")
|
||||
}
|
||||
key1, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||
key2, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if _, err := NewSigningKeyRing([]SigningKeyMaterial{
|
||||
{Algorithm: domain.TelegramLoginSigningRS256, PrivateKey: key1, Active: true},
|
||||
{Algorithm: domain.TelegramLoginSigningRS256, PrivateKey: key2, Active: true},
|
||||
}, nil); err == nil {
|
||||
t.Fatal("two active RS256 keys unexpectedly accepted")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue