-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathauth.go
164 lines (136 loc) · 4.31 KB
/
auth.go
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
// Package ginkeycloak provides a Gin Middleware for authorizing a client with a client credential grant and scope
package ginkeycloak
import (
"encoding/json"
"errors"
"net/http"
"net/url"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-resty/resty/v2"
)
// RequestContext is an interface to satisfy the needs of the internal auth requests
type RequestContext interface {
GetHeader(key string) string
AbortWithStatusJSON(code int, jsonObj interface{})
Next()
}
// AuthLogger is a minimal logger interface that is used inside the package
type AuthLogger interface {
Errorf(format string, args ...interface{})
Tracef(format string, args ...interface{})
Error(args ...interface{})
}
// Auth is the struct to hold details for the client credentials grant and provides the middleware function
type Auth struct {
clientID string
clientSecret string
host string
port string
scheme string
scope string
log AuthLogger
tokenIntrospectPath string
restyClient *resty.Client
}
// New creates an Auth instance with a client for verifying tokens
func New(id, secret, host, port, scheme, scope, tokenIntrospectPath string, logger AuthLogger) *Auth {
rc := resty.New()
a := &Auth{
id,
secret,
host,
port,
scheme,
scope,
logger,
tokenIntrospectPath,
rc,
}
if a.scheme == "" {
a.scheme = "https"
}
if a.port == "" {
a.port = "443"
}
return a
}
const (
tokenHeaderParts = 2
errorAuthHeaderIncorrectOrInvalidString = "authorization header incorrect or invalid, wrong number of parts"
unknownErrorString = "unknown error encountered while trying to validate token"
)
var (
errorAuthHeaderIncorrectOrInvalid = errors.New(errorAuthHeaderIncorrectOrInvalidString)
ginUnknownErrorReturn = gin.H{"error": unknownErrorString}
)
// ClientDetails are the minimum, necessary client details we need from the token
type ClientDetails struct {
Active bool `json:"active"`
Scope string `json:"scope"`
ClientID string `json:"clientId"`
}
func (a *Auth) getRawToken(authorizationHeader string) (string, error) {
parts := strings.Split(authorizationHeader, " ")
if len(parts) != tokenHeaderParts {
a.log.Errorf("Authorization header incorrect: parts = %+v", parts)
return "", errorAuthHeaderIncorrectOrInvalid
}
if parts[0] != "Bearer" {
a.log.Errorf("Authorization header incorrect: parts = %+v", parts)
return "", errorAuthHeaderIncorrectOrInvalid
}
return parts[1], nil
}
// HandleFunc is the Gin middleware function for handling client authentication via Bearer token
func (a *Auth) HandleFunc(context *gin.Context) {
a.handleFuncInternal(context)
}
// VerifyTokenFromHeader will verify an Authorization string of the format "Bearer ......"
func (a *Auth) VerifyTokenFromHeader(header string) (bool, *ClientDetails, error) {
token, err := a.getRawToken(header)
if err != nil {
// context.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return false, nil, err
}
realHost := a.host
if a.port != "" {
realHost = a.host + ":" + a.port
}
urlVar := url.URL{
Scheme: a.scheme,
Host: realHost,
Path: a.tokenIntrospectPath,
}
req := a.restyClient.R()
req.SetBasicAuth(a.clientID, a.clientSecret).
SetHeader("Content-Type", "application/x-www-form-urlencoded").
SetBody([]byte("token=" + token))
a.log.Tracef(urlVar.String())
resp, err := req.Post(urlVar.String())
if err != nil {
return false, nil, err
}
var cd ClientDetails
err = json.Unmarshal(resp.Body(), &cd)
if err != nil {
a.log.Error(err)
return false, nil, err
} else if cd.Active && strings.Contains(cd.Scope, a.scope) {
a.log.Tracef("Authorized client: %s", cd.ClientID)
return true, &cd, nil
}
return false, nil, errors.New("unknown error encountered while trying to validate token")
}
func (a *Auth) handleFuncInternal(context RequestContext) {
h := context.GetHeader("Authorization")
authorized, clientDetails, err := a.VerifyTokenFromHeader(h)
if err != nil {
context.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
} else if authorized && clientDetails != nil {
context.Next()
return
}
context.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": ginUnknownErrorReturn})
}