refactor!: external auth for ACME Challenges, and other mgmt routes

This commit is contained in:
AJ ONeal 2022-06-07 02:26:30 -06:00
parent 8536e20406
commit a7f1398ba4
Signed by: coolaj86
GPG Key ID: 585419CA6DB0AA23
13 changed files with 512 additions and 430 deletions

View File

@ -10,6 +10,7 @@ import (
"os" "os"
"strings" "strings"
"git.rootprojects.org/root/telebit/internal/acmeroutes"
"git.rootprojects.org/root/telebit/internal/mgmt" "git.rootprojects.org/root/telebit/internal/mgmt"
"git.rootprojects.org/root/telebit/internal/mgmt/authstore" "git.rootprojects.org/root/telebit/internal/mgmt/authstore"
@ -34,7 +35,7 @@ var (
serviceName = "telebit-mgmt" serviceName = "telebit-mgmt"
// serviceDesc // serviceDesc
serviceDesc = "Telebit Device Management" //serviceDesc = "Telebit Device Management"
) )
func ver() string { func ver() string {
@ -151,7 +152,12 @@ func main() {
_ = store.SetMaster(secret) _ = store.SetMaster(secret)
defer store.Close() defer store.Close()
mgmt.Init(store, provider) var authURL string
if 0 == len(authURL) {
authURL = os.Getenv("AUTH_URL")
}
mgmt.Init(store)
acmeroutes.Init(provider)
if len(challengesPort) > 0 { if len(challengesPort) > 0 {
go func() { go func() {
@ -178,7 +184,7 @@ func main() {
r.Get("/api/version", func(w http.ResponseWriter, r *http.Request) { r.Get("/api/version", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("TODO (json): " + ver() + "\n")) w.Write([]byte("TODO (json): " + ver() + "\n"))
}) })
mgmt.RouteAll(r) mgmt.RouteAll(r, authURL)
fmt.Fprintf(os.Stderr, "failed: %s", http.ListenAndServe(lnAddr, r)) fmt.Fprintf(os.Stderr, "failed: %s", http.ListenAndServe(lnAddr, r))
} }

View File

@ -709,7 +709,7 @@ func fetchDirectivesAndRun() {
return return
} }
err = mgmt.Ping(config.authURL, token) err = authutil.Ping(config.authURL, token)
if nil != err { if nil != err {
fmt.Fprintf(os.Stderr, "failed to ping mgmt server: %s\n", err) fmt.Fprintf(os.Stderr, "failed to ping mgmt server: %s\n", err)
//os.Exit(exitRetry) //os.Exit(exitRetry)
@ -722,7 +722,7 @@ func fetchDirectivesAndRun() {
// re-create token unless no secret was supplied // re-create token unless no secret was supplied
token, err = authstore.HMACToken(config.pairwiseSecret, config.leeway) token, err = authstore.HMACToken(config.pairwiseSecret, config.leeway)
} }
err = mgmt.Ping(config.authURL, token) err = authutil.Ping(config.authURL, token)
if nil != err { if nil != err {
fmt.Fprintf(os.Stderr, "failed to ping mgmt server: %s\n", err) fmt.Fprintf(os.Stderr, "failed to ping mgmt server: %s\n", err)
//os.Exit(exitRetry) //os.Exit(exitRetry)

View File

@ -1,4 +1,4 @@
package mgmt package acmeroutes
import ( import (
"context" "context"
@ -6,8 +6,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"path/filepath"
"strings" "strings"
"git.rootprojects.org/root/telebit/internal/authutil"
"git.rootprojects.org/root/telebit/internal/http01fs" "git.rootprojects.org/root/telebit/internal/http01fs"
"github.com/go-acme/lego/v3/challenge" "github.com/go-acme/lego/v3/challenge"
@ -37,11 +39,50 @@ type Identifier struct {
Value string `json:"value"` Value string `json:"value"`
} }
/*
type acmeProvider struct { type acmeProvider struct {
BaseURL string BaseURL string
provider challenge.Provider provider challenge.Provider
} }
*/
var provider challenge.Provider = nil
var presenters = make(chan *Challenge)
var cleanups = make(chan *Challenge)
// Init initializes some package variables
func Init(p challenge.Provider) {
provider = p
go func() {
for {
// TODO make parallel?
// TODO make cancellable?
ch := <-presenters
if nil != provider {
err := provider.Present(ch.Domain, ch.Token, ch.KeyAuth)
ch.error <- err
} else {
ch.error <- fmt.Errorf("missing acme challenge provider for present")
}
}
}()
go func() {
for {
// TODO make parallel?
// TODO make cancellable?
ch := <-cleanups
if nil != provider {
ch.error <- provider.CleanUp(ch.Domain, ch.Token, ch.KeyAuth)
} else {
ch.error <- fmt.Errorf("missing acme challenge provider for cleanup")
}
}
}()
}
/*
func (p *acmeProvider) Present(domain, token, keyAuth string) error { func (p *acmeProvider) Present(domain, token, keyAuth string) error {
return p.provider.Present(domain, token, keyAuth) return p.provider.Present(domain, token, keyAuth)
} }
@ -49,8 +90,33 @@ func (p *acmeProvider) Present(domain, token, keyAuth string) error {
func (p *acmeProvider) CleanUp(domain, token, keyAuth string) error { func (p *acmeProvider) CleanUp(domain, token, keyAuth string) error {
return p.provider.CleanUp(domain, token, keyAuth) return p.provider.CleanUp(domain, token, keyAuth)
} }
*/
func handleACMEChallengeRoutes(r chi.Router) { // GetACMEChallenges fetches stored HTTP-01 challenges
func GetACMEChallenges(w http.ResponseWriter, r *http.Request) {
//token := chi.URLParam(r, "token")
host := r.Host
/*
// TODO TrustProxy option?
xHost := r.Header.Get("X-Forwarded-Host")
//log.Printf("[debug] Host: %q\n[debug] X-Host: %q", host, xHost)
if len(xHost) > 0 {
host = xHost
}
*/
// disallow FS characters
if strings.ContainsAny(host, "/:|\\") {
host = ""
}
tokenPath := filepath.Join(tmpBase, host)
fsrv := http.FileServer(http.Dir(tokenPath))
fsrv.ServeHTTP(w, r)
}
// HandleACMEChallengeRoutes allows storing ACME challenges for relay
func HandleACMEChallengeRoutes(r chi.Router) {
handleACMEChallenges := func(r chi.Router) { handleACMEChallenges := func(r chi.Router) {
r.Post("/{domain}", createChallenge) r.Post("/{domain}", createChallenge)
@ -78,7 +144,7 @@ func createChallenge(w http.ResponseWriter, r *http.Request) {
domain := chi.URLParam(r, "domain") domain := chi.URLParam(r, "domain")
ctx := r.Context() ctx := r.Context()
claims, ok := ctx.Value(MWKey("claims")).(*MgmtClaims) claims, ok := ctx.Value(authutil.MWKey("claims")).(*authutil.Claims)
if !ok || !isSlugAllowed(domain, claims.Slug) { if !ok || !isSlugAllowed(domain, claims.Slug) {
msg := `{ "error": "invalid domain", "code":"E_BAD_REQUEST"}` msg := `{ "error": "invalid domain", "code":"E_BAD_REQUEST"}`
http.Error(w, msg+"\n", http.StatusUnprocessableEntity) http.Error(w, msg+"\n", http.StatusUnprocessableEntity)
@ -103,8 +169,8 @@ func createChallenge(w http.ResponseWriter, r *http.Request) {
if "" == ch.Token || "" == ch.KeyAuth { if "" == ch.Token || "" == ch.KeyAuth {
err = errors.New("missing token and/or key auth") err = errors.New("missing token and/or key auth")
} else if strings.Contains(ch.Type, "http") { } else if strings.Contains(ch.Type, "http") {
provider := &http01fs.Provider http01Provider := &http01fs.Provider
provider.Present(context.Background(), acme.Challenge{ http01Provider.Present(context.Background(), acme.Challenge{
Token: ch.Token, Token: ch.Token,
KeyAuthorization: ch.KeyAuth, KeyAuthorization: ch.KeyAuth,
Identifier: acme.Identifier{ Identifier: acme.Identifier{
@ -146,8 +212,8 @@ func deleteChallenge(w http.ResponseWriter, r *http.Request) {
if "" == ch.Token || "" == ch.KeyAuth { if "" == ch.Token || "" == ch.KeyAuth {
err = errors.New("missing token and/or key auth") err = errors.New("missing token and/or key auth")
} else if strings.Contains(ch.Type, "http") { } else if strings.Contains(ch.Type, "http") {
provider := &http01fs.Provider http01Provider := &http01fs.Provider
provider.CleanUp(context.Background(), acme.Challenge{ http01Provider.CleanUp(context.Background(), acme.Challenge{
Token: ch.Token, Token: ch.Token,
KeyAuthorization: ch.KeyAuth, KeyAuthorization: ch.KeyAuth,
Identifier: acme.Identifier{ Identifier: acme.Identifier{

View File

@ -0,0 +1,85 @@
package authutil
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"git.rootprojects.org/root/telebit/internal/dbg"
)
// Authorizer is called when a new client connects and we need to know something about it
type Authorizer func(*http.Request) (*Grants, error)
var NotAuthorizedContent = []byte("{ \"error\": \"not authorized\" }\n")
// NewAuthorizer will create a new (proxiable) token verifier
func NewAuthorizer(authURL string) Authorizer {
return func(r *http.Request) (*Grants, error) {
// do we have a valid wss_client?
fmt.Printf("[authz] Authorization = %s\n", r.Header.Get("Authorization"))
var tokenString string
if auth := strings.Split(r.Header.Get("Authorization"), " "); len(auth) > 1 {
// TODO handle Basic auth tokens as well
tokenString = auth[1]
}
if "" == tokenString {
// Browsers do not allow Authorization Headers and must use access_token query string
tokenString = r.URL.Query().Get("access_token")
}
if "" != r.URL.Query().Get("access_token") {
r.URL.Query().Set("access_token", "[redacted]")
}
fmt.Printf("[authz] authURL = %s\n", authURL)
fmt.Printf("[authz] token = %s\n", tokenString)
grants, err := Inspect(authURL, tokenString)
if nil != err {
fmt.Printf("[authorizer] error inspecting %q: %s\ntoken: %s\n", authURL, err, tokenString)
return nil, err
}
if "" != r.URL.Query().Get("access_token") {
r.URL.Query().Set("access_token", "[redacted:"+grants.Subject+"]")
}
return grants, err
}
}
// Grants are verified token Claims
type Grants struct {
Subject string `json:"sub"`
Audience string `json:"aud"`
Domains []string `json:"domains"`
Ports []int `json:"ports"`
}
// Inspect will verify a token and return its details, decoded
func Inspect(authURL, token string) (*Grants, error) {
inspectURL := strings.TrimSuffix(authURL, "/inspect") + "/inspect"
if dbg.Debug {
fmt.Fprintf(os.Stderr, "[debug] telebit.Inspect(\n\tinspectURL = %s,\n\ttoken = %s,\n)\n", inspectURL, token)
}
msg, err := Request("GET", inspectURL, token, nil)
if nil != err {
return nil, err
}
if nil == msg {
return nil, fmt.Errorf("invalid response")
}
grants := &Grants{}
err = json.NewDecoder(msg).Decode(grants)
if err != nil {
return nil, err
}
if "" == grants.Subject {
fmt.Fprintf(os.Stderr, "TODO update mgmt server to show Subject: %q\n", msg)
grants.Subject = strings.Split(grants.Domains[0], ".")[0]
}
return grants, nil
}

View File

@ -0,0 +1,12 @@
package authutil
import "github.com/dgrijalva/jwt-go"
// MWKey is a type guard for context.Value
type MWKey string
// Claims includes a Slug, for backwards compatibility
type Claims struct {
Slug string `json:"slug"`
jwt.StandardClaims
}

View File

@ -0,0 +1,68 @@
package authutil
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"time"
)
type SuccessResponse struct {
Success bool `json:"success"`
}
// Request makes an HTTP request the way we like...
func Request(method, fullurl, token string, payload io.Reader) (io.Reader, error) {
HTTPClient := &http.Client{
Timeout: 15 * time.Second,
}
req, err := http.NewRequest(method, fullurl, payload)
if err != nil {
return nil, err
}
if len(token) > 0 {
req.Header.Set("Authorization", "Bearer "+token)
}
if nil != payload {
req.Header.Set("Content-Type", "application/json")
}
resp, err := HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("%d: failed to read response body: %w", resp.StatusCode, err)
}
if resp.StatusCode >= http.StatusBadRequest {
return nil, fmt.Errorf("%d: request failed: %v", resp.StatusCode, string(body))
}
return bytes.NewBuffer(body), nil
}
func Ping(authURL, token string) error {
msg, err := Request("POST", authURL+"/ping", token, nil)
if nil != err {
return err
}
if nil == msg {
return fmt.Errorf("invalid response")
}
resp := SuccessResponse{}
err = json.NewDecoder(msg).Decode(&resp)
if err != nil {
return err
}
if true != resp.Success {
return fmt.Errorf("expected successful response")
}
return nil
}

View File

@ -7,34 +7,11 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"git.rootprojects.org/root/telebit/internal/authutil"
"git.rootprojects.org/root/telebit/internal/dbg" "git.rootprojects.org/root/telebit/internal/dbg"
"git.rootprojects.org/root/telebit/internal/mgmt/authstore" "git.rootprojects.org/root/telebit/internal/mgmt/authstore"
"git.rootprojects.org/root/telebit/internal/telebit"
) )
type SuccessResponse struct {
Success bool `json:"success"`
}
func Ping(authURL, token string) error {
msg, err := telebit.Request("POST", authURL+"/ping", token, nil)
if nil != err {
return err
}
if nil == msg {
return fmt.Errorf("invalid response")
}
resp := SuccessResponse{}
err = json.NewDecoder(msg).Decode(&resp)
if err != nil {
return err
}
if true != resp.Success {
return fmt.Errorf("expected successful response")
}
return nil
}
func Register(authURL, secret, ppid string) (kid string, err error) { func Register(authURL, secret, ppid string) (kid string, err error) {
pub := authstore.ToPublicKeyString(ppid) pub := authstore.ToPublicKeyString(ppid)
jsons := fmt.Sprintf(`{ "machine_ppid": "%s", "public_key": "%s" }`, ppid, pub) jsons := fmt.Sprintf(`{ "machine_ppid": "%s", "public_key": "%s" }`, ppid, pub)
@ -43,7 +20,7 @@ func Register(authURL, secret, ppid string) (kid string, err error) {
if dbg.Debug { if dbg.Debug {
fmt.Fprintf(os.Stderr, "[debug] authURL=%s, secret=%s, ppid=%s\n", fullURL, secret, jsons) fmt.Fprintf(os.Stderr, "[debug] authURL=%s, secret=%s, ppid=%s\n", fullURL, secret, jsons)
} }
msg, err := telebit.Request("POST", fullURL, "", jsonb) msg, err := authutil.Request("POST", fullURL, "", jsonb)
if nil != err { if nil != err {
return "", err return "", err
} }

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"time" "time"
"git.rootprojects.org/root/telebit/internal/authutil"
"git.rootprojects.org/root/telebit/internal/mgmt/authstore" "git.rootprojects.org/root/telebit/internal/mgmt/authstore"
"github.com/go-chi/chi" "github.com/go-chi/chi"
@ -21,7 +22,7 @@ func handleDeviceRoutes(r chi.Router) {
r.Use(func(next http.Handler) http.Handler { r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
claims, ok := ctx.Value(MWKey("claims")).(*MgmtClaims) claims, ok := ctx.Value(authutil.MWKey("claims")).(*authutil.Claims)
if !ok || "*" != claims.Slug { if !ok || "*" != claims.Slug {
msg := `{"error":"missing or invalid authorization token", "code":"E_TOKEN"}` msg := `{"error":"missing or invalid authorization token", "code":"E_TOKEN"}`
http.Error(w, msg+"\n", http.StatusUnprocessableEntity) http.Error(w, msg+"\n", http.StatusUnprocessableEntity)

View File

@ -2,14 +2,10 @@ package mgmt
import ( import (
"git.rootprojects.org/root/telebit/internal/mgmt/authstore" "git.rootprojects.org/root/telebit/internal/mgmt/authstore"
"github.com/go-acme/lego/v3/challenge"
) )
var store authstore.Store var store authstore.Store
var provider challenge.Provider = nil
// DeviceDomain is the base hostname used for devices, such as devices.example.com // DeviceDomain is the base hostname used for devices, such as devices.example.com
// which has devices as foo.devices.example.com // which has devices as foo.devices.example.com
var DeviceDomain string var DeviceDomain string
@ -18,11 +14,7 @@ var DeviceDomain string
// ( currently NOT used, but will be used for wss://RELAY_DOMAIN/ ) // ( currently NOT used, but will be used for wss://RELAY_DOMAIN/ )
var RelayDomain string var RelayDomain string
// MWKey is a type guard
type MWKey string
// Init initializes some package variables // Init initializes some package variables
func Init(s authstore.Store, p challenge.Provider) { func Init(s authstore.Store) {
store = s store = s
provider = p
} }

View File

@ -7,10 +7,11 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strings" "strings"
"time" "time"
"git.rootprojects.org/root/telebit/internal/acmeroutes"
"git.rootprojects.org/root/telebit/internal/authutil"
"git.rootprojects.org/root/telebit/internal/dbg" "git.rootprojects.org/root/telebit/internal/dbg"
"git.rootprojects.org/root/telebit/internal/mgmt/authstore" "git.rootprojects.org/root/telebit/internal/mgmt/authstore"
@ -19,15 +20,7 @@ import (
"github.com/go-chi/chi/middleware" "github.com/go-chi/chi/middleware"
) )
// MgmtClaims includes a Slug, for backwards compatibility // RouteStatic handles the not-so-dynamic routes
type MgmtClaims struct {
Slug string `json:"slug"`
jwt.StandardClaims
}
var presenters = make(chan *Challenge)
var cleanups = make(chan *Challenge)
func RouteStatic(r chi.Router) chi.Router { func RouteStatic(r chi.Router) chi.Router {
r.Route("/", func(r chi.Router) { r.Route("/", func(r chi.Router) {
@ -35,72 +28,72 @@ func RouteStatic(r chi.Router) chi.Router {
r.Use(middleware.Timeout(15 * time.Second)) r.Use(middleware.Timeout(15 * time.Second))
r.Use(middleware.Recoverer) r.Use(middleware.Recoverer)
r.Get("/.well-known/acme-challenge/{token}", getACMEChallenges) r.Get("/.well-known/acme-challenge/{token}", acmeroutes.GetACMEChallenges)
}) })
return r return r
} }
func getACMEChallenges(w http.ResponseWriter, r *http.Request) { // RouteAll will generate the routes
//token := chi.URLParam(r, "token") func RouteAll(r chi.Router, authURL string) {
host := r.Host
/*
// TODO TrustProxy option?
xHost := r.Header.Get("X-Forwarded-Host")
//log.Printf("[debug] Host: %q\n[debug] X-Host: %q", host, xHost)
if len(xHost) > 0 {
host = xHost
}
*/
// disallow FS characters
if strings.ContainsAny(host, "/:|\\") {
host = ""
}
tokenPath := filepath.Join(tmpBase, host)
fsrv := http.FileServer(http.Dir(tokenPath))
fsrv.ServeHTTP(w, r)
}
func RouteAll(r chi.Router) {
go func() {
for {
// TODO make parallel?
// TODO make cancellable?
ch := <-presenters
if nil != provider {
err := provider.Present(ch.Domain, ch.Token, ch.KeyAuth)
ch.error <- err
} else {
ch.error <- fmt.Errorf("missing acme challenge provider for present")
}
}
}()
go func() {
for {
// TODO make parallel?
// TODO make cancellable?
ch := <-cleanups
if nil != provider {
ch.error <- provider.CleanUp(ch.Domain, ch.Token, ch.KeyAuth)
} else {
ch.error <- fmt.Errorf("missing acme challenge provider for cleanup")
}
}
}()
r.Route("/", func(r chi.Router) { r.Route("/", func(r chi.Router) {
r.Use(middleware.Logger) r.Use(middleware.Logger)
r.Use(middleware.Timeout(15 * time.Second)) r.Use(middleware.Timeout(15 * time.Second))
r.Use(middleware.Recoverer) r.Use(middleware.Recoverer)
r.Get("/.well-known/acme-challenge/{token}", getACMEChallenges) r.Get("/.well-known/acme-challenge/{token}", acmeroutes.GetACMEChallenges)
r.Route("/api", func(r chi.Router) { r.Route("/api", func(r chi.Router) {
r.Use(func(next http.Handler) http.Handler { r.Use(newClaimsGetter(authURL))
acmeroutes.HandleACMEChallengeRoutes(r)
handleDeviceRoutes(r)
if len(authURL) > 0 {
fmt.Printf("Delegating auth to %s\nIgnoring '/api/inspect'\n", authURL)
} else {
r.Get("/inspect", inspectToken)
}
r.Route("/register-device", registerDevice)
r.Post("/ping", pong)
r.Get("/", hello)
})
})
}
// TODO consolidate 'newClaimsGetter' and 'getVerifiedClaims'
func newClaimsGetter(authURL string) func(next http.Handler) http.Handler {
if 0 == len(authURL) {
return getVerifiedClaims
}
authorizer := authutil.NewAuthorizer(authURL)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
grants, err := authorizer(r)
if nil != err {
log.Println("authorization failed", err)
w.Write(authutil.NotAuthorizedContent)
return
}
// TODO use Claims rather than Grants
claims := &authutil.Claims{
Slug: grants.Subject,
StandardClaims: jwt.StandardClaims{
Subject: grants.Subject,
Audience: grants.Audience,
},
}
ctx := r.Context()
ctx = context.WithValue(ctx, authutil.MWKey("claims"), claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func getVerifiedClaims(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -113,7 +106,7 @@ func RouteAll(r chi.Router) {
//var err2 error = nil //var err2 error = nil
tok, err := jwt.ParseWithClaims( tok, err := jwt.ParseWithClaims(
tokenString, tokenString,
&MgmtClaims{}, &authutil.Claims{},
func(token *jwt.Token) (interface{}, error) { func(token *jwt.Token) (interface{}, error) {
if dbg.Debug { if dbg.Debug {
fmt.Println("parsed jwt", token) fmt.Println("parsed jwt", token)
@ -127,7 +120,7 @@ func RouteAll(r chi.Router) {
return nil, fmt.Errorf("invalid jwt header 'kid' (key id)") return nil, fmt.Errorf("invalid jwt header 'kid' (key id)")
} }
claims := token.Claims.(*MgmtClaims) claims := token.Claims.(*authutil.Claims)
jti := claims.Id jti := claims.Id
if "" == jti { if "" == jti {
return nil, fmt.Errorf("missing jwt payload 'jti' (jwt id / nonce)") return nil, fmt.Errorf("missing jwt payload 'jti' (jwt id / nonce)")
@ -169,30 +162,27 @@ func RouteAll(r chi.Router) {
ctx := r.Context() ctx := r.Context()
if nil != tok { if nil != tok {
if tok.Valid { if tok.Valid {
ctx = context.WithValue(ctx, MWKey("token"), tok) ctx = context.WithValue(ctx, authutil.MWKey("token"), tok)
ctx = context.WithValue(ctx, MWKey("claims"), tok.Claims) ctx = context.WithValue(ctx, authutil.MWKey("claims"), tok.Claims)
ctx = context.WithValue(ctx, MWKey("valid"), true) ctx = context.WithValue(ctx, authutil.MWKey("valid"), true)
fmt.Println("[auth] Token is fully valid:") fmt.Println("[auth] Token is fully valid:")
fmt.Println(ctx.Value(MWKey("claims"))) fmt.Println(ctx.Value(authutil.MWKey("claims")))
} else { } else {
fmt.Println("[auth] Token was parsed, but NOT valid:", tok) fmt.Println("[auth] Token was parsed, but NOT valid:", tok)
} }
} }
if nil != err { if nil != err {
fmt.Println("[auth] Token is NOT valid due to error:", err) fmt.Println("[auth] Token is NOT valid due to error:", err)
ctx = context.WithValue(ctx, MWKey("error"), err) ctx = context.WithValue(ctx, authutil.MWKey("error"), err)
} }
next.ServeHTTP(w, r.WithContext(ctx)) next.ServeHTTP(w, r.WithContext(ctx))
}) })
}) }
handleACMEChallengeRoutes(r) func inspectToken(w http.ResponseWriter, r *http.Request) {
handleDeviceRoutes(r)
r.Get("/inspect", func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
claims, ok := ctx.Value(MWKey("claims")).(*MgmtClaims) claims, ok := ctx.Value(authutil.MWKey("claims")).(*authutil.Claims)
if !ok { if !ok {
msg := `{"error":"failure to ping: 3", "code":"E_TOKEN"}` msg := `{"error":"failure to ping: 3", "code":"E_TOKEN"}`
fmt.Println("touch no claims", claims) fmt.Println("touch no claims", claims)
@ -207,9 +197,9 @@ func RouteAll(r chi.Router) {
claims.Slug, claims.Slug,
DeviceDomain, DeviceDomain,
))) )))
}) }
r.Route("/register-device", func(r chi.Router) { func registerDevice(r chi.Router) {
// r.Use() // must NOT have slug '*' // r.Use() // must NOT have slug '*'
r.Post("/{otp}", func(w http.ResponseWriter, r *http.Request) { r.Post("/{otp}", func(w http.ResponseWriter, r *http.Request) {
@ -273,11 +263,11 @@ func RouteAll(r chi.Router) {
result, _ := json.Marshal(auth) result, _ := json.Marshal(auth)
w.Write(result) w.Write(result)
}) })
}) }
r.Post("/ping", func(w http.ResponseWriter, r *http.Request) { func pong(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
claims, ok := ctx.Value(MWKey("claims")).(*MgmtClaims) claims, ok := ctx.Value(authutil.MWKey("claims")).(*authutil.Claims)
if !ok { if !ok {
msg := `{"error":"failure to ping: 1", "code":"E_TOKEN"}` msg := `{"error":"failure to ping: 1", "code":"E_TOKEN"}`
http.Error(w, msg+"\n", http.StatusBadRequest) http.Error(w, msg+"\n", http.StatusBadRequest)
@ -300,11 +290,8 @@ func RouteAll(r chi.Router) {
} }
w.Write([]byte(`{ "success": true }` + "\n")) w.Write([]byte(`{ "success": true }` + "\n"))
}) }
r.Get("/", func(w http.ResponseWriter, r *http.Request) { func hello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello\n")) w.Write([]byte("Hello\n"))
})
})
})
} }

View File

@ -14,6 +14,7 @@ import (
"time" "time"
"git.rootprojects.org/root/telebit/assets/admin" "git.rootprojects.org/root/telebit/assets/admin"
"git.rootprojects.org/root/telebit/internal/authutil"
"git.rootprojects.org/root/telebit/internal/dbg" "git.rootprojects.org/root/telebit/internal/dbg"
"github.com/go-chi/chi" "github.com/go-chi/chi"
@ -88,14 +89,14 @@ func RouteAdmin(authURL string, r chi.Router) {
grants, err := authorizer(r) grants, err := authorizer(r)
if nil != err { if nil != err {
log.Println("authorization failed", err) log.Println("authorization failed", err)
w.Write(apiNotAuthorizedContent) w.Write(authutil.NotAuthorizedContent)
return return
} }
// TODO define Admins in a better way // TODO define Admins in a better way
if "*" != grants.Subject { if "*" != grants.Subject {
log.Println("only admins allowed", err) log.Println("only admins allowed", err)
w.Write(apiNotAuthorizedContent) w.Write(authutil.NotAuthorizedContent)
return return
} }
@ -121,7 +122,6 @@ func RouteAdmin(authURL string, r chi.Router) {
} }
var apiNotFoundContent = []byte("{ \"error\": \"not found\" }\n") var apiNotFoundContent = []byte("{ \"error\": \"not found\" }\n")
var apiNotAuthorizedContent = []byte("{ \"error\": \"not authorized\" }\n")
func apiNotFoundHandler(w http.ResponseWriter, r *http.Request) { func apiNotFoundHandler(w http.ResponseWriter, r *http.Request) {
w.Write(apiNotFoundContent) w.Write(apiNotFoundContent)
@ -232,7 +232,7 @@ func upgradeWebsocket(w http.ResponseWriter, r *http.Request) {
grants, err := authorizer(r) grants, err := authorizer(r)
if nil != err { if nil != err {
log.Println("WebSocket authorization failed", err) log.Println("WebSocket authorization failed", err)
w.Write(apiNotAuthorizedContent) w.Write(authutil.NotAuthorizedContent)
return return
} }

View File

@ -1,41 +0,0 @@
package telebit
import (
"fmt"
"net/http"
"strings"
)
func NewAuthorizer(authURL string) Authorizer {
return func(r *http.Request) (*Grants, error) {
// do we have a valid wss_client?
fmt.Printf("[authz] Authorization = %s\n", r.Header.Get("Authorization"))
var tokenString string
if auth := strings.Split(r.Header.Get("Authorization"), " "); len(auth) > 1 {
// TODO handle Basic auth tokens as well
tokenString = auth[1]
}
if "" == tokenString {
// Browsers do not allow Authorization Headers and must use access_token query string
tokenString = r.URL.Query().Get("access_token")
}
if "" != r.URL.Query().Get("access_token") {
r.URL.Query().Set("access_token", "[redacted]")
}
fmt.Printf("[authz] authURL = %s\n", authURL)
fmt.Printf("[authz] token = %s\n", tokenString)
grants, err := Inspect(authURL, tokenString)
if nil != err {
fmt.Printf("[authorizer] error inspecting %q: %s\ntoken: %s\n", authURL, err, tokenString)
return nil, err
}
if "" != r.URL.Query().Get("access_token") {
r.URL.Query().Set("access_token", "[redacted:"+grants.Subject+"]")
}
return grants, err
}
}

View File

@ -1,13 +1,10 @@
package telebit package telebit
import ( import (
"bytes"
"crypto/tls" "crypto/tls"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
@ -16,7 +13,6 @@ import (
"strings" "strings"
"time" "time"
"git.rootprojects.org/root/telebit/internal/dbg"
httpshim "git.rootprojects.org/root/telebit/internal/tunnel" httpshim "git.rootprojects.org/root/telebit/internal/tunnel"
"github.com/coolaj86/certmagic" "github.com/coolaj86/certmagic"
@ -48,9 +44,6 @@ type Handler interface {
// HandlerFunc should handle, proxy, or terminate the connection // HandlerFunc should handle, proxy, or terminate the connection
type HandlerFunc func(net.Conn) error type HandlerFunc func(net.Conn) error
// Authorizer is called when a new client connects and we need to know something about it
type Authorizer func(*http.Request) (*Grants, error)
// Serve calls f(conn). // Serve calls f(conn).
func (f HandlerFunc) Serve(conn net.Conn) error { func (f HandlerFunc) Serve(conn net.Conn) error {
return f(conn) return f(conn)
@ -415,6 +408,7 @@ func TerminateTLS(client net.Conn, acme *ACME) net.Conn {
} }
} }
// NewCertMagic creates our flavor of a CertMagic instance
func NewCertMagic(acme *ACME) (*certmagic.Config, error) { func NewCertMagic(acme *ACME) (*certmagic.Config, error) {
if !acme.Agree { if !acme.Agree {
fmt.Fprintf( fmt.Fprintf(
@ -458,68 +452,3 @@ func NewCertMagic(acme *ACME) (*certmagic.Config, error) {
magic.Issuer = certmagic.NewACMEManager(magic, manager) magic.Issuer = certmagic.NewACMEManager(magic, manager)
return magic, nil return magic, nil
} }
type Grants struct {
Subject string `json:"sub"`
Audience string `json:"aud"`
Domains []string `json:"domains"`
Ports []int `json:"ports"`
}
func Inspect(authURL, token string) (*Grants, error) {
inspectURL := strings.TrimSuffix(authURL, "/inspect") + "/inspect"
if dbg.Debug {
fmt.Fprintf(os.Stderr, "[debug] telebit.Inspect(\n\tinspectURL = %s,\n\ttoken = %s,\n)\n", inspectURL, token)
}
msg, err := Request("GET", inspectURL, token, nil)
if nil != err {
return nil, err
}
if nil == msg {
return nil, fmt.Errorf("invalid response")
}
grants := &Grants{}
err = json.NewDecoder(msg).Decode(grants)
if err != nil {
return nil, err
}
if "" == grants.Subject {
fmt.Fprintf(os.Stderr, "TODO update mgmt server to show Subject: %q\n", msg)
grants.Subject = strings.Split(grants.Domains[0], ".")[0]
}
return grants, nil
}
func Request(method, fullurl, token string, payload io.Reader) (io.Reader, error) {
HTTPClient := &http.Client{
Timeout: 15 * time.Second,
}
req, err := http.NewRequest(method, fullurl, payload)
if err != nil {
return nil, err
}
if len(token) > 0 {
req.Header.Set("Authorization", "Bearer "+token)
}
if nil != payload {
req.Header.Set("Content-Type", "application/json")
}
resp, err := HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("%d: failed to read response body: %w", resp.StatusCode, err)
}
if resp.StatusCode >= http.StatusBadRequest {
return nil, fmt.Errorf("%d: request failed: %v", resp.StatusCode, string(body))
}
return bytes.NewBuffer(body), nil
}