summaryrefslogtreecommitdiff
path: root/internal/handlers/handler_firstfactor_spnego.go
blob: 954665b0836f6e19a05bf452cb44c38c1c0c86fb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package handlers

import (
	"encoding/base64"
	"net/http"
	"strings"

	"github.com/authelia/authelia/v4/internal/middlewares"
	"github.com/valyala/fasthttp"
	"gopkg.in/jcmturner/goidentity.v3"
	"gopkg.in/jcmturner/gokrb5.v7/gssapi"
	"gopkg.in/jcmturner/gokrb5.v7/keytab"
	"gopkg.in/jcmturner/gokrb5.v7/service"
	"gopkg.in/jcmturner/gokrb5.v7/spnego"
	"gopkg.in/jcmturner/gokrb5.v7/types"
)

const (
	// spnegoNegTokenRespKRBAcceptCompleted - The response on successful authentication always has this header. Capturing as const so we don't have marshaling and encoding overhead.
	spnegoNegTokenRespKRBAcceptCompleted = "Negotiate oRQwEqADCgEAoQsGCSqGSIb3EgECAg=="
	// spnegoNegTokenRespReject - The response on a failed authentication always has this rejection header. Capturing as const so we don't have marshaling and encoding overhead.
	spnegoNegTokenRespReject = "Negotiate oQcwBaADCgEC"
	// spnegoNegTokenRespIncompleteKRB5 - Response token specifying incomplete context and KRB5 as the supported mechtype.
	spnegoNegTokenRespIncompleteKRB5 = "Negotiate oRQwEqADCgEBoQsGCSqGSIb3EgECAg=="
)

// spnego.SPNEGOKRB5Authenticate is a Kerberos spnego.SPNEGO authentication HTTP handler wrapper.
func FirstFactorSPNEGO(inner fasthttp.RequestHandler, kt *keytab.Keytab, settings ...func(*service.Settings)) middlewares.RequestHandler {
	return func(ctx *middlewares.AutheliaCtx) {
		// Get the auth header
		s := strings.SplitN(string(ctx.Request.Header.Peek(spnego.HTTPHeaderAuthRequest)), " ", 2)
		if len(s) != 2 || s[0] != spnego.HTTPHeaderAuthResponseValueKey {
			// No Authorization header set so return 401 with WWW-Authenticate Negotiate header
			ctx.Response.Header.Set(spnego.HTTPHeaderAuthResponse, spnego.HTTPHeaderAuthResponseValueKey)
			ctx.Response.SetStatusCode(http.StatusUnauthorized)
			ctx.Response.SetBodyString(spnego.UnauthorizedMsg)
			return
		}

		// Set up the spnego.SPNEGO GSS-API mechanism
		var SPNEGO *spnego.SPNEGO
		h, err := types.GetHostAddress(ctx.RemoteAddr().String())
		if err == nil {
			// put in this order so that if the user provides a ClientAddress it will override the one here.
			o := append([]func(*service.Settings){service.ClientAddress(h)}, settings...)
			SPNEGO = spnego.SPNEGOService(kt, o...)
		} else {
			SPNEGO = spnego.SPNEGOService(kt, settings...)
			SPNEGO.Log("%s - spnego.SPNEGO could not parse client address: %v", ctx.RemoteAddr(), err)
		}

		// Decode the header into an spnego.SPNEGO context token
		b, err := base64.StdEncoding.DecodeString(s[1])
		if err != nil {
			SPNEGONegotiateKRB5MechType(SPNEGO, ctx, "%s - spnego.SPNEGO error in base64 decoding negotiation header: %v", ctx.RemoteAddr(), err)
			return
		}
		var st spnego.SPNEGOToken
		err = st.Unmarshal(b)
		if err != nil {
			SPNEGONegotiateKRB5MechType(SPNEGO, ctx, "%s - spnego.SPNEGO error in unmarshaling spnego.SPNEGO token: %v", ctx.RemoteAddr(), err)
			return
		}

		// Validate the context token
		authed, context, status := SPNEGO.AcceptSecContext(&st)
		if status.Code != gssapi.StatusComplete && status.Code != gssapi.StatusContinueNeeded {
			SPNEGOResponseReject(SPNEGO, ctx, "%s - spnego.SPNEGO validation error: %v", ctx.RemoteAddr(), status)
			return
		}

		if status.Code == gssapi.StatusContinueNeeded {
			SPNEGONegotiateKRB5MechType(SPNEGO, ctx, "%s - spnego.SPNEGO GSS-API continue needed", ctx.RemoteAddr())
			return
		}

		if authed {
			_ = context.Value(spnego.CTXKeyCredentials).(goidentity.Identity)

			ctx.Response.Header.Set(spnego.HTTPHeaderAuthResponse, spnegoNegTokenRespKRBAcceptCompleted)

		} else {
			SPNEGOResponseReject(SPNEGO, ctx, "%s - spnego.SPNEGO Kerberos authentication failed", ctx.RemoteAddr())
			return
		}
	}
}

func SPNEGONegotiateKRB5MechType(s *spnego.SPNEGO, ctx *middlewares.AutheliaCtx, format string, v ...interface{}) {
	s.Log(format, v...)
	ctx.Response.Header.Set(spnego.HTTPHeaderAuthResponse, spnegoNegTokenRespIncompleteKRB5)
	ctx.Response.SetStatusCode(http.StatusUnauthorized)
	ctx.Response.SetBodyString(spnego.UnauthorizedMsg)
}

func SPNEGOResponseReject(s *spnego.SPNEGO, ctx *middlewares.AutheliaCtx, format string, v ...interface{}) {
	s.Log(format, v...)
	ctx.Response.Header.Set(spnego.HTTPHeaderAuthResponse, spnegoNegTokenRespReject)
	ctx.Response.SetStatusCode(http.StatusUnauthorized)
	ctx.Response.SetBodyString(spnego.UnauthorizedMsg)
}