-
Notifications
You must be signed in to change notification settings - Fork 500
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add Kerberos auth support #702
base: master
Are you sure you want to change the base?
Changes from 16 commits
1854c5c
f2e846d
b46e98e
1f85924
2fe0bca
d361509
86e0074
eca4758
c47a35b
27f50f1
f792281
ca67c06
6e619be
d4f52ce
530eb45
85c5bb1
04c18be
8059af5
79d6641
b4e96e3
729e190
dc1a816
73ae20f
bf01def
dd22d87
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
package mssql | ||
|
||
import ( | ||
"fmt" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/jcmturner/gokrb5/v8/client" | ||
"github.com/jcmturner/gokrb5/v8/config" | ||
"github.com/jcmturner/gokrb5/v8/credentials" | ||
"github.com/jcmturner/gokrb5/v8/keytab" | ||
"github.com/jcmturner/gokrb5/v8/spnego" | ||
) | ||
|
||
type krb5Auth struct { | ||
username string | ||
realm string | ||
serverSPN string | ||
password string | ||
port uint64 | ||
krb5Config *config.Config | ||
krbKeytab *keytab.Keytab | ||
krbCache *credentials.CCache | ||
krb5Client *client.Client | ||
state krb5ClientState | ||
} | ||
|
||
func getKRB5Auth(user, password, serverSPN string, krb5Conf *config.Config, keytabContent *keytab.Keytab, cacheContent *credentials.CCache) (auth, bool) { | ||
var port uint64 | ||
var realm, serviceStr string | ||
var err error | ||
|
||
params1 := strings.Split(serverSPN, ":") | ||
if len(params1) != 2 { | ||
return nil, false | ||
} | ||
|
||
params2 := strings.Split(params1[1], "@") | ||
switch len(params2) { | ||
case 1: | ||
port, err = strconv.ParseUint(params1[1], 10, 16) | ||
if err != nil { | ||
return nil, false | ||
} | ||
case 2: | ||
port, err = strconv.ParseUint(params2[0], 10, 16) | ||
if err != nil { | ||
return nil, false | ||
} | ||
default: | ||
return nil, false | ||
} | ||
|
||
params3 := strings.Split(serverSPN, "@") | ||
switch len(params3) { | ||
case 1: | ||
serviceStr = params3[0] | ||
params3 = strings.Split(params1[0], "/") | ||
params3 = strings.Split(params3[1], ".") | ||
realm = params3[1] + "." + params3[2] | ||
case 2: | ||
realm = params3[1] | ||
serviceStr = params3[0] | ||
default: | ||
return nil, false | ||
} | ||
|
||
return &krb5Auth{ | ||
username: user, | ||
serverSPN: serviceStr, | ||
port: port, | ||
realm: realm, | ||
krb5Config: krb5Conf, | ||
krbKeytab: keytabContent, | ||
krbCache: cacheContent, | ||
password: password, | ||
}, true | ||
} | ||
|
||
func (auth *krb5Auth) InitialBytes() ([]byte, error) { | ||
var cl *client.Client | ||
var err error | ||
// Init keytab from conf | ||
if auth.krbKeytab != nil { | ||
// Init krb5 client and login | ||
cl = client.NewWithKeytab(auth.username, auth.realm, auth.krbKeytab, auth.krb5Config, client.DisablePAFXFAST(true)) | ||
} else { | ||
cl, err = client.NewFromCCache(auth.krbCache, auth.krb5Config) | ||
if err != nil { | ||
return []byte{}, err | ||
} | ||
} | ||
auth.krb5Client = cl | ||
auth.state = initiatorStart | ||
tkt, sessionKey, err := cl.GetServiceTicket(auth.serverSPN) | ||
if err != nil { | ||
return []byte{}, err | ||
} | ||
|
||
negTok, err := spnego.NewNegTokenInitKRB5(auth.krb5Client, tkt, sessionKey) | ||
if err != nil { | ||
return []byte{}, err | ||
} | ||
|
||
outToken, err := negTok.Marshal() | ||
if err != nil { | ||
return []byte{}, err | ||
} | ||
auth.state = initiatorWaitForMutal | ||
return outToken, nil | ||
} | ||
|
||
func (auth *krb5Auth) Free() { | ||
auth.krb5Client.Destroy() | ||
} | ||
|
||
func (auth *krb5Auth) NextBytes(token []byte) ([]byte, error) { | ||
var spnegoToken spnego.SPNEGOToken | ||
if err := spnegoToken.Unmarshal(token); err != nil { | ||
err := fmt.Errorf("unmarshal APRep token failed: %w", err) | ||
return []byte{}, err | ||
} | ||
auth.state = initiatorReady | ||
return []byte{}, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
package mssql | ||
|
||
import ( | ||
"reflect" | ||
"testing" | ||
|
||
"github.com/jcmturner/gokrb5/v8/client" | ||
"github.com/jcmturner/gokrb5/v8/config" | ||
"github.com/jcmturner/gokrb5/v8/credentials" | ||
"github.com/jcmturner/gokrb5/v8/keytab" | ||
) | ||
|
||
func TestGetKRB5Auth(t *testing.T) { | ||
krbConf := &config.Config{} | ||
krbKeytab := &keytab.Keytab{} | ||
krbCache := &credentials.CCache{} | ||
|
||
got, _ := getKRB5Auth("", "", "MSSQLSvc/mssql.domain.com:1433", krbConf, krbKeytab, krbCache) | ||
keytab := &krb5Auth{username: "", | ||
realm: "domain.com", | ||
serverSPN: "MSSQLSvc/mssql.domain.com:1433", | ||
password: "", | ||
port: 1433, | ||
krb5Config: krbConf, | ||
krbKeytab: krbKeytab, | ||
krbCache: krbCache, | ||
state: 0} | ||
|
||
res := reflect.DeepEqual(got, keytab) | ||
if !res { | ||
t.Errorf("Failed to get correct krb5Auth object\nExpected:%v\nRecieved:%v", keytab, got) | ||
} | ||
|
||
got, _ = getKRB5Auth("", "", "MSSQLSvc/mssql.domain.com:1433", krbConf, krbKeytab, krbCache) | ||
keytab = &krb5Auth{username: "", | ||
realm: "domain.com", | ||
serverSPN: "MSSQLSvc/mssql.domain.com:1433", | ||
password: "", | ||
port: 1433, | ||
krb5Config: krbConf, | ||
krbKeytab: krbKeytab, | ||
krbCache: krbCache, | ||
state: 0} | ||
|
||
res = reflect.DeepEqual(got, keytab) | ||
if !res { | ||
t.Errorf("Failed to get correct krb5Auth object\nExpected:%v\nRecieved:%v", keytab, got) | ||
} | ||
|
||
_, val := getKRB5Auth("", "", "MSSQLSvc/mssql.domain.com", krbConf, krbKeytab, krbCache) | ||
if val { | ||
t.Errorf("Failed to get correct krb5Auth object: no port defined") | ||
} | ||
|
||
got, _ = getKRB5Auth("", "", "MSSQLSvc/mssql.domain.com:[email protected]", krbConf, krbKeytab, krbCache) | ||
keytab = &krb5Auth{username: "", | ||
realm: "DOMAIN.COM", | ||
serverSPN: "MSSQLSvc/mssql.domain.com:1433", | ||
password: "", | ||
port: 1433, | ||
krb5Config: krbConf, | ||
krbKeytab: krbKeytab, | ||
krbCache: krbCache, | ||
state: 0} | ||
|
||
res = reflect.DeepEqual(got, keytab) | ||
if !res { | ||
t.Errorf("Failed to get correct krb5Auth object\nExpected:%v\nRecieved:%v", keytab, got) | ||
} | ||
|
||
_, val = getKRB5Auth("", "", "MSSQLSvc/mssql.domain.com:[email protected]@test", krbConf, krbKeytab, krbCache) | ||
if val { | ||
t.Errorf("Failed to get correct krb5Auth object due to incorrect serverSPN name") | ||
} | ||
|
||
_, val = getKRB5Auth("", "", "MSSQLSvc/mssql.domain.com:[email protected]", krbConf, krbKeytab, krbCache) | ||
if val { | ||
t.Errorf("Failed to get correct krb5Auth object due to incorrect port") | ||
} | ||
|
||
_, val = getKRB5Auth("", "", "MSSQLSvc/mssql.domain.com:port", krbConf, krbKeytab, krbCache) | ||
if val { | ||
t.Errorf("Failed to get correct krb5Auth object due to incorrect port") | ||
} | ||
} | ||
|
||
func TestInitialBytes(t *testing.T) { | ||
krbConf := &config.Config{} | ||
krbKeytab := &keytab.Keytab{} | ||
krbCache := &credentials.CCache{} | ||
krbObj := &krb5Auth{username: "", | ||
realm: "domain.com", | ||
serverSPN: "MSSQLSvc/mssql.domain.com:1433", | ||
password: "", | ||
port: 1433, | ||
krb5Config: krbConf, | ||
krbKeytab: krbKeytab, | ||
krbCache: krbCache, | ||
state: 0, | ||
} | ||
|
||
_, err := krbObj.InitialBytes() | ||
if err == nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the test is expecting a nil err please change the |
||
t.Errorf("Failed to get Initial bytes") | ||
} | ||
|
||
//krbObj.initkrbwithkeytab = true | ||
_, err = krbObj.InitialBytes() | ||
if err == nil { | ||
t.Errorf("Failed to get Initial bytes") | ||
} | ||
} | ||
|
||
func TestNextBytes(t *testing.T) { | ||
ans := []byte{} | ||
krbConf := &config.Config{} | ||
krbKeytab := &keytab.Keytab{} | ||
krbCache := &credentials.CCache{} | ||
|
||
var krbObj auth = &krb5Auth{username: "", | ||
realm: "domain.com", | ||
serverSPN: "MSSQLSvc/mssql.domain.com:1433", | ||
password: "", | ||
port: 1433, | ||
krb5Config: krbConf, | ||
krbKeytab: krbKeytab, | ||
krbCache: krbCache, | ||
state: 0} | ||
|
||
_, err := krbObj.NextBytes(ans) | ||
if err == nil { | ||
t.Errorf("Error getting next byte") | ||
} | ||
} | ||
|
||
func TestFree(t *testing.T) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a test should have at least one assertion |
||
krbConf := &config.Config{} | ||
krbKeytab := &keytab.Keytab{} | ||
krbCache := &credentials.CCache{} | ||
kt := &keytab.Keytab{} | ||
c := &config.Config{} | ||
cl := client.NewWithKeytab("Administrator", "DOMAIN.COM", kt, c, client.DisablePAFXFAST(true)) | ||
|
||
var krbObj auth = &krb5Auth{username: "", | ||
realm: "domain.com", | ||
serverSPN: "MSSQLSvc/mssql.domain.com:1433", | ||
password: "", | ||
port: 1433, | ||
krb5Config: krbConf, | ||
krbKeytab: krbKeytab, | ||
krbCache: krbCache, | ||
state: 0, | ||
krb5Client: cl, | ||
} | ||
krbObj.Free() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree 1.13 is probably a reasonable minimum requirement at this point.
That said - it may be worthwhile to decouple specific auth implementations like this one from msdsn in general and instead have each auth implementation inject itself into a discovery mechanism such as with some visitor pattern on the connection properties. I'm new enough to Go not to know the best way to have such dependency injection.
With such dependency injection you could mark the kerbauth files for 1.13+ only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How could this PR align with #718 ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shueybubbles with 718, anyone using go-mssql driver for kerberos will have to implement their own custom logic and/or maintain their own repos to handle the entire kerberos authN process.
Moreover my PR provides with flexibility to use either krb cache or keytab file which I believe is missing from 718?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My other comment re: coupling the config to the specific krb implementation is my main concern. The other PR at least avoids such coupling. I think there should be a way for the backend of the driver to manage krb more transparently than having the client use krb-specific packages to even define the connection.