authenticator.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. package service
  2. import (
  3. "encoding/base64"
  4. "errors"
  5. "fmt"
  6. "strings"
  7. "time"
  8. goidentity "gopkg.in/jcmturner/goidentity.v3"
  9. "gopkg.in/jcmturner/gokrb5.v6/client"
  10. "gopkg.in/jcmturner/gokrb5.v6/config"
  11. "gopkg.in/jcmturner/gokrb5.v6/credentials"
  12. "gopkg.in/jcmturner/gokrb5.v6/gssapi"
  13. "gopkg.in/jcmturner/gokrb5.v6/keytab"
  14. )
  15. // SPNEGOAuthenticator implements gopkg.in/jcmturner/goidentity.v3.Authenticator interface
  16. type SPNEGOAuthenticator struct {
  17. SPNEGOHeaderValue string
  18. ClientAddr string
  19. Config *Config
  20. }
  21. // Config for service side implementation
  22. //
  23. // Keytab (mandatory) - keytab for the service user
  24. //
  25. // KeytabPrincipal (optional) - keytab principal override for the service.
  26. // The service looks for this principal in the keytab to use to decrypt tickets.
  27. // If "" is passed as KeytabPrincipal then the principal will be automatically derived
  28. // from the service name (SName) and realm in the ticket the service is trying to decrypt.
  29. // This is often sufficient if you create the SPN in MIT KDC with: /usr/sbin/kadmin.local -q "add_principal HTTP/<fqdn>"
  30. // When Active Directory is used for the KDC this may need to be the account name you have set the SPN against
  31. // (setspn.exe -a "HTTP/<fqdn>" <account name>)
  32. // If you are unsure run:
  33. //
  34. // klist -k <service's keytab file>
  35. //
  36. // and use the value from the Principal column for the keytab entry the service should use.
  37. //
  38. // RequireHostAddr - require that the kerberos ticket must include client host IP addresses and one must match the client making the request.
  39. // This is controlled in the client config with the noaddresses option (http://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html).
  40. //
  41. // DisablePACDecoding - if set to true decoding of the Microsoft PAC will be disabled.
  42. type Config struct {
  43. Keytab keytab.Keytab
  44. ServicePrincipal string
  45. RequireHostAddr bool
  46. DisablePACDecoding bool
  47. }
  48. // NewSPNEGOAuthenticator creates a new SPNEGOAuthenticator.
  49. func NewSPNEGOAuthenticator(kt keytab.Keytab) (a SPNEGOAuthenticator) {
  50. a.Config = NewConfig(kt)
  51. return
  52. }
  53. // NewConfig creates a new kerberos service Config.
  54. func NewConfig(kt keytab.Keytab) *Config {
  55. return &Config{Keytab: kt}
  56. }
  57. // Authenticate performs authentication checks against the negotiation header value provided.
  58. func (c *Config) Authenticate(neg, addr string) (i goidentity.Identity, ok bool, err error) {
  59. a := SPNEGOAuthenticator{
  60. SPNEGOHeaderValue: neg,
  61. ClientAddr: addr,
  62. Config: c,
  63. }
  64. b, err := base64.StdEncoding.DecodeString(a.SPNEGOHeaderValue)
  65. if err != nil {
  66. err = fmt.Errorf("SPNEGO error in base64 decoding negotiation header: %v", err)
  67. return
  68. }
  69. var spnego gssapi.SPNEGO
  70. err = spnego.Unmarshal(b)
  71. if !spnego.Init {
  72. err = fmt.Errorf("SPNEGO negotiation token is not a NegTokenInit: %v", err)
  73. return
  74. }
  75. if !(spnego.NegTokenInit.MechTypes[0].Equal(gssapi.MechTypeOIDKRB5) ||
  76. spnego.NegTokenInit.MechTypes[0].Equal(gssapi.MechTypeOIDMSLegacyKRB5)) {
  77. err = errors.New("SPNEGO OID of MechToken is not of type KRB5")
  78. return
  79. }
  80. var mt gssapi.MechToken
  81. err = mt.Unmarshal(spnego.NegTokenInit.MechToken)
  82. if err != nil {
  83. err = fmt.Errorf("SPNEGO error unmarshaling MechToken: %v", err)
  84. return
  85. }
  86. if !mt.IsAPReq() {
  87. err = errors.New("MechToken does not contain an AP_REQ - KRB_AP_ERR_MSG_TYPE")
  88. return
  89. }
  90. ok, creds, err := ValidateAPREQ(mt.APReq, a)
  91. if err != nil {
  92. err = fmt.Errorf("SPNEGO validation error: %v", err)
  93. return
  94. }
  95. i = &creds
  96. return
  97. }
  98. // Authenticate and retrieve a goidentity.Identity. In this case it is a pointer to a credentials.Credentials
  99. func (a SPNEGOAuthenticator) Authenticate() (i goidentity.Identity, ok bool, err error) {
  100. return a.Config.Authenticate(a.SPNEGOHeaderValue, a.ClientAddr)
  101. }
  102. // Mechanism returns the authentication mechanism.
  103. func (a SPNEGOAuthenticator) Mechanism() string {
  104. return "SPNEGO Kerberos"
  105. }
  106. // KRB5BasicAuthenticator implements gopkg.in/jcmturner/goidentity.v3.Authenticator interface.
  107. // It takes username and password so can be used for basic authentication.
  108. type KRB5BasicAuthenticator struct {
  109. SPN string
  110. BasicHeaderValue string
  111. ServiceConfig Config
  112. ClientConfig *config.Config
  113. realm string
  114. username string
  115. password string
  116. }
  117. // Authenticate and return the identity. The boolean indicates if the authentication was successful.
  118. func (a KRB5BasicAuthenticator) Authenticate() (i goidentity.Identity, ok bool, err error) {
  119. a.realm, a.username, a.password, err = parseBasicHeaderValue(a.BasicHeaderValue)
  120. if err != nil {
  121. err = fmt.Errorf("could not parse basic authentication header: %v", err)
  122. return
  123. }
  124. cl := client.NewClientWithPassword(a.username, a.realm, a.password)
  125. cl.WithConfig(a.ClientConfig)
  126. err = cl.Login()
  127. if err != nil {
  128. // Username and/or password could be wrong
  129. err = fmt.Errorf("error with user credentials during login: %v", err)
  130. return
  131. }
  132. tkt, _, err := cl.GetServiceTicket(a.SPN)
  133. if err != nil {
  134. err = fmt.Errorf("could not get service ticket: %v", err)
  135. return
  136. }
  137. err = tkt.DecryptEncPart(a.ServiceConfig.Keytab, a.ServiceConfig.ServicePrincipal)
  138. if err != nil {
  139. err = fmt.Errorf("could not decrypt service ticket: %v", err)
  140. return
  141. }
  142. cl.Credentials.SetAuthTime(time.Now().UTC())
  143. cl.Credentials.SetAuthenticated(true)
  144. isPAC, pac, err := tkt.GetPACType(a.ServiceConfig.Keytab, a.ServiceConfig.ServicePrincipal)
  145. if isPAC && err != nil {
  146. err = fmt.Errorf("error processing PAC: %v", err)
  147. return
  148. }
  149. if isPAC {
  150. // There is a valid PAC. Adding attributes to creds
  151. cl.Credentials.SetADCredentials(credentials.ADCredentials{
  152. GroupMembershipSIDs: pac.KerbValidationInfo.GetGroupMembershipSIDs(),
  153. LogOnTime: pac.KerbValidationInfo.LogOnTime.Time(),
  154. LogOffTime: pac.KerbValidationInfo.LogOffTime.Time(),
  155. PasswordLastSet: pac.KerbValidationInfo.PasswordLastSet.Time(),
  156. EffectiveName: pac.KerbValidationInfo.EffectiveName.Value,
  157. FullName: pac.KerbValidationInfo.FullName.Value,
  158. UserID: int(pac.KerbValidationInfo.UserID),
  159. PrimaryGroupID: int(pac.KerbValidationInfo.PrimaryGroupID),
  160. LogonServer: pac.KerbValidationInfo.LogonServer.Value,
  161. LogonDomainName: pac.KerbValidationInfo.LogonDomainName.Value,
  162. LogonDomainID: pac.KerbValidationInfo.LogonDomainID.String(),
  163. })
  164. }
  165. ok = true
  166. i = cl.Credentials
  167. return
  168. }
  169. // Mechanism returns the authentication mechanism.
  170. func (a KRB5BasicAuthenticator) Mechanism() string {
  171. return "Kerberos Basic"
  172. }
  173. func parseBasicHeaderValue(s string) (domain, username, password string, err error) {
  174. b, err := base64.StdEncoding.DecodeString(s)
  175. if err != nil {
  176. return
  177. }
  178. v := string(b)
  179. vc := strings.SplitN(v, ":", 2)
  180. password = vc[1]
  181. // Domain and username can be specified in 2 formats:
  182. // <Username> - no domain specified
  183. // <Domain>\<Username>
  184. // <Username>@<Domain>
  185. if strings.Contains(vc[0], `\`) {
  186. u := strings.SplitN(vc[0], `\`, 2)
  187. domain = u[0]
  188. username = u[1]
  189. } else if strings.Contains(vc[0], `@`) {
  190. u := strings.SplitN(vc[0], `@`, 2)
  191. domain = u[1]
  192. username = u[0]
  193. } else {
  194. username = vc[0]
  195. }
  196. return
  197. }