package spnego
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"github.com/jcmturner/gofork/encoding/asn1"
"github.com/jcmturner/goidentity/v6"
"github.com/jcmturner/gokrb5/v8/client"
"github.com/jcmturner/gokrb5/v8/credentials"
"github.com/jcmturner/gokrb5/v8/gssapi"
"github.com/jcmturner/gokrb5/v8/iana/nametype"
"github.com/jcmturner/gokrb5/v8/keytab"
"github.com/jcmturner/gokrb5/v8/krberror"
"github.com/jcmturner/gokrb5/v8/service"
"github.com/jcmturner/gokrb5/v8/types"
)
type Client struct {
*http .Client
krb5Client *client .Client
spn string
reqs []*http .Request
}
type redirectErr struct {
reqTarget *http .Request
}
func (e redirectErr ) Error () string {
return fmt .Sprintf ("redirect to %v" , e .reqTarget .URL )
}
type teeReadCloser struct {
io .Reader
io .Closer
}
func NewClient (krb5Cl *client .Client , httpCl *http .Client , spn string ) *Client {
if httpCl == nil {
httpCl = &http .Client {}
}
if httpCl .Jar == nil {
httpCl .Jar , _ = cookiejar .New (nil )
}
f := httpCl .CheckRedirect
httpCl .CheckRedirect = func (req *http .Request , via []*http .Request ) error {
if f != nil {
err := f (req , via )
if err != nil {
return err
}
}
return redirectErr {reqTarget : req }
}
return &Client {
Client : httpCl ,
krb5Client : krb5Cl ,
spn : spn ,
}
}
func (c *Client ) Do (req *http .Request ) (resp *http .Response , err error ) {
var body bytes .Buffer
if req .Body != nil {
teeR := io .TeeReader (req .Body , &body )
teeRC := teeReadCloser {teeR , req .Body }
req .Body = teeRC
}
resp , err = c .Client .Do (req )
if err != nil {
if ue , ok := err .(*url .Error ); ok {
if e , ok := ue .Err .(redirectErr ); ok {
e .reqTarget .Header .Del (HTTPHeaderAuthRequest )
c .reqs = append (c .reqs , e .reqTarget )
if len (c .reqs ) >= 10 {
return resp , errors .New ("stopped after 10 redirects" )
}
if req .Body != nil {
e .reqTarget .Body = io .NopCloser (&body )
}
return c .Do (e .reqTarget )
}
}
return resp , err
}
if respUnauthorizedNegotiate (resp ) {
err := SetSPNEGOHeader (c .krb5Client , req , c .spn )
if err != nil {
return resp , err
}
if req .Body != nil {
req .Body = io .NopCloser (&body )
}
io .Copy (io .Discard , resp .Body )
resp .Body .Close ()
return c .Do (req )
}
return resp , err
}
func (c *Client ) Get (url string ) (resp *http .Response , err error ) {
req , err := http .NewRequest ("GET" , url , nil )
if err != nil {
return nil , err
}
return c .Do (req )
}
func (c *Client ) Post (url , contentType string , body io .Reader ) (resp *http .Response , err error ) {
req , err := http .NewRequest ("POST" , url , body )
if err != nil {
return nil , err
}
req .Header .Set ("Content-Type" , contentType )
return c .Do (req )
}
func (c *Client ) PostForm (url string , data url .Values ) (resp *http .Response , err error ) {
return c .Post (url , "application/x-www-form-urlencoded" , strings .NewReader (data .Encode ()))
}
func (c *Client ) Head (url string ) (resp *http .Response , err error ) {
req , err := http .NewRequest ("HEAD" , url , nil )
if err != nil {
return nil , err
}
return c .Do (req )
}
func respUnauthorizedNegotiate(resp *http .Response ) bool {
if resp .StatusCode == http .StatusUnauthorized {
if resp .Header .Get (HTTPHeaderAuthResponse ) == HTTPHeaderAuthResponseValueKey {
return true
}
}
return false
}
func setRequestSPN(r *http .Request ) (types .PrincipalName , error ) {
h := strings .TrimSuffix (r .URL .Host , "." )
if strings .LastIndex (r .URL .Host , ":" ) > strings .LastIndex (r .URL .Host , "]" ) {
h , p , err := net .SplitHostPort (h )
if err != nil {
return types .PrincipalName {}, err
}
name , err := net .LookupCNAME (h )
if name != "" && err == nil {
h = strings .ToLower (name )
}
h = strings .TrimSuffix (h , "." )
r .Host = fmt .Sprintf ("%s:%s" , h , p )
return types .NewPrincipalName (nametype .KRB_NT_PRINCIPAL , "HTTP/" +h ), nil
}
name , err := net .LookupCNAME (h )
if name != "" && err == nil {
h = strings .ToLower (name )
}
h = strings .TrimSuffix (h , "." )
r .Host = h
return types .NewPrincipalName (nametype .KRB_NT_PRINCIPAL , "HTTP/" +h ), nil
}
func SetSPNEGOHeader (cl *client .Client , r *http .Request , spn string ) error {
if spn == "" {
pn , err := setRequestSPN (r )
if err != nil {
return err
}
spn = pn .PrincipalNameString ()
}
cl .Log ("using SPN %s" , spn )
s := SPNEGOClient (cl , spn )
err := s .AcquireCred ()
if err != nil {
return fmt .Errorf ("could not acquire client credential: %v" , err )
}
st , err := s .InitSecContext ()
if err != nil {
return fmt .Errorf ("could not initialize context: %v" , err )
}
nb , err := st .Marshal ()
if err != nil {
return krberror .Errorf (err , krberror .EncodingError , "could not marshal SPNEGO" )
}
hs := "Negotiate " + base64 .StdEncoding .EncodeToString (nb )
r .Header .Set (HTTPHeaderAuthRequest , hs )
return nil
}
const (
spnegoNegTokenRespKRBAcceptCompleted = "Negotiate oRQwEqADCgEAoQsGCSqGSIb3EgECAg=="
spnegoNegTokenRespReject = "Negotiate oQcwBaADCgEC"
spnegoNegTokenRespIncompleteKRB5 = "Negotiate oRQwEqADCgEBoQsGCSqGSIb3EgECAg=="
sessionCredentials = "github.com/jcmturner/gokrb5/v8/sessionCredentials"
ctxCredentials = "github.com/jcmturner/gokrb5/v8/ctxCredentials"
HTTPHeaderAuthRequest = "Authorization"
HTTPHeaderAuthResponse = "WWW-Authenticate"
HTTPHeaderAuthResponseValueKey = "Negotiate"
UnauthorizedMsg = "Unauthorised.\n"
)
func SPNEGOKRB5Authenticate (inner http .Handler , kt *keytab .Keytab , settings ...func (*service .Settings )) http .Handler {
return http .HandlerFunc (func (w http .ResponseWriter , r *http .Request ) {
var spnego *SPNEGO
h , err := types .GetHostAddress (r .RemoteAddr )
if err == nil {
o := append ([]func (*service .Settings ){service .ClientAddress (h )}, settings ...)
spnego = SPNEGOService (kt , o ...)
} else {
spnego = SPNEGOService (kt , settings ...)
spnego .Log ("%s - SPNEGO could not parse client address: %v" , r .RemoteAddr , err )
}
id , err := getSessionCredentials (spnego , r )
if err == nil && id .Authenticated () {
spnego .Log ("%s - SPNEGO request served under session %s" , r .RemoteAddr , id .SessionID ())
inner .ServeHTTP (w , goidentity .AddToHTTPRequestContext (&id , r ))
return
}
st , err := getAuthorizationNegotiationHeaderAsSPNEGOToken (spnego , r , w )
if st == nil || err != nil {
return
}
authed , ctx , status := spnego .AcceptSecContext (st )
if status .Code != gssapi .StatusComplete && status .Code != gssapi .StatusContinueNeeded {
spnegoResponseReject (spnego , w , "%s - SPNEGO validation error: %v" , r .RemoteAddr , status )
return
}
if status .Code == gssapi .StatusContinueNeeded {
spnegoNegotiateKRB5MechType (spnego , w , "%s - SPNEGO GSS-API continue needed" , r .RemoteAddr )
return
}
if authed {
id := ctx .Value (ctxCredentials ).(*credentials .Credentials )
err = newSession (spnego , r , w , id )
if err != nil {
return
}
spnegoResponseAcceptCompleted (spnego , w , "%s %s@%s - SPNEGO authentication succeeded" , r .RemoteAddr , id .UserName (), id .Domain ())
inner .ServeHTTP (w , goidentity .AddToHTTPRequestContext (id , r ))
return
}
spnegoResponseReject (spnego , w , "%s - SPNEGO Kerberos authentication failed" , r .RemoteAddr )
return
})
}
func getAuthorizationNegotiationHeaderAsSPNEGOToken(spnego *SPNEGO , r *http .Request , w http .ResponseWriter ) (*SPNEGOToken , error ) {
s := strings .SplitN (r .Header .Get (HTTPHeaderAuthRequest ), " " , 2 )
if len (s ) != 2 || s [0 ] != HTTPHeaderAuthResponseValueKey {
w .Header ().Set (HTTPHeaderAuthResponse , HTTPHeaderAuthResponseValueKey )
http .Error (w , UnauthorizedMsg , http .StatusUnauthorized )
return nil , errors .New ("client did not provide a negotiation authorization header" )
}
b , err := base64 .StdEncoding .DecodeString (s [1 ])
if err != nil {
err = fmt .Errorf ("error in base64 decoding negotiation header: %v" , err )
spnegoNegotiateKRB5MechType (spnego , w , "%s - SPNEGO %v" , r .RemoteAddr , err )
return nil , err
}
var st SPNEGOToken
err = st .Unmarshal (b )
if err != nil {
var k5t KRB5Token
if k5t .Unmarshal (b ) != nil {
err = fmt .Errorf ("error in unmarshaling SPNEGO token: %v" , err )
spnegoNegotiateKRB5MechType (spnego , w , "%s - SPNEGO %v" , r .RemoteAddr , err )
return nil , err
}
st .Init = true
st .NegTokenInit = NegTokenInit {
MechTypes : []asn1 .ObjectIdentifier {k5t .OID },
MechTokenBytes : b ,
}
}
return &st , nil
}
func getSessionCredentials(spnego *SPNEGO , r *http .Request ) (credentials .Credentials , error ) {
var creds credentials .Credentials
if sm := spnego .serviceSettings .SessionManager (); sm != nil {
cb , err := sm .Get (r , sessionCredentials )
if err != nil || cb == nil || len (cb ) < 1 {
return creds , fmt .Errorf ("%s - SPNEGO error getting session and credentials for request: %v" , r .RemoteAddr , err )
}
err = creds .Unmarshal (cb )
if err != nil {
return creds , fmt .Errorf ("%s - SPNEGO credentials malformed in session: %v" , r .RemoteAddr , err )
}
return creds , nil
}
return creds , errors .New ("no session manager configured" )
}
func newSession(spnego *SPNEGO , r *http .Request , w http .ResponseWriter , id *credentials .Credentials ) error {
if sm := spnego .serviceSettings .SessionManager (); sm != nil {
idb , err := id .Marshal ()
if err != nil {
spnegoInternalServerError (spnego , w , "SPNEGO could not marshal credentials to add to the session: %v" , err )
return err
}
err = sm .New (w , r , sessionCredentials , idb )
if err != nil {
spnegoInternalServerError (spnego , w , "SPNEGO could not create new session: %v" , err )
return err
}
spnego .Log ("%s %s@%s - SPNEGO new session (%s) created" , r .RemoteAddr , id .UserName (), id .Domain (), id .SessionID ())
}
return nil
}
func spnegoNegotiateKRB5MechType(s *SPNEGO , w http .ResponseWriter , format string , v ...interface {}) {
s .Log (format , v ...)
w .Header ().Set (HTTPHeaderAuthResponse , spnegoNegTokenRespIncompleteKRB5 )
http .Error (w , UnauthorizedMsg , http .StatusUnauthorized )
}
func spnegoResponseReject(s *SPNEGO , w http .ResponseWriter , format string , v ...interface {}) {
s .Log (format , v ...)
w .Header ().Set (HTTPHeaderAuthResponse , spnegoNegTokenRespReject )
http .Error (w , UnauthorizedMsg , http .StatusUnauthorized )
}
func spnegoResponseAcceptCompleted(s *SPNEGO , w http .ResponseWriter , format string , v ...interface {}) {
s .Log (format , v ...)
w .Header ().Set (HTTPHeaderAuthResponse , spnegoNegTokenRespKRBAcceptCompleted )
}
func spnegoInternalServerError(s *SPNEGO , w http .ResponseWriter , format string , v ...interface {}) {
s .Log (format , v ...)
http .Error (w , "Internal Server Error" , http .StatusInternalServerError )
}
The pages are generated with Golds v0.6.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @Go100and1 (reachable from the left QR code) to get the latest news of Golds .