Add post token implementation
Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
This commit is contained in:
parent
98620458e3
commit
1126e32234
4 changed files with 260 additions and 18 deletions
|
@ -3,8 +3,11 @@ package main
|
|||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/context"
|
||||
|
@ -73,15 +76,20 @@ func main() {
|
|||
logrus.Fatalf("Error initializing access controller: %v", err)
|
||||
}
|
||||
|
||||
// TODO: Make configurable
|
||||
issuer.Expiration = 15 * time.Minute
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
ts := &tokenServer{
|
||||
issuer: issuer,
|
||||
accessController: ac,
|
||||
refreshCache: map[string]refreshToken{},
|
||||
}
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.Path("/token/").Methods("GET").Handler(handlerWithContext(ctx, ts.getToken))
|
||||
router.Path("/token/").Methods("POST").Handler(handlerWithContext(ctx, ts.postToken))
|
||||
|
||||
if cert == "" {
|
||||
err = http.ListenAndServe(addr, router)
|
||||
|
@ -120,9 +128,52 @@ func handleError(ctx context.Context, err error, w http.ResponseWriter) {
|
|||
context.GetResponseLogger(ctx).Info("application error")
|
||||
}
|
||||
|
||||
var refreshCharacters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
const refreshTokenLength = 15
|
||||
|
||||
func newRefreshToken() string {
|
||||
s := make([]rune, refreshTokenLength)
|
||||
for i := range s {
|
||||
s[i] = refreshCharacters[rand.Intn(len(refreshCharacters))]
|
||||
}
|
||||
return string(s)
|
||||
}
|
||||
|
||||
type refreshToken struct {
|
||||
subject string
|
||||
service string
|
||||
}
|
||||
|
||||
type tokenServer struct {
|
||||
issuer *TokenIssuer
|
||||
accessController auth.AccessController
|
||||
refreshCache map[string]refreshToken
|
||||
}
|
||||
|
||||
type tokenResponse struct {
|
||||
Token string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
ExpiresIn int `json:"expires_in,omitempty"`
|
||||
}
|
||||
|
||||
func filterAccessList(ctx context.Context, scope string, requestedAccessList []auth.Access) []auth.Access {
|
||||
if !strings.HasSuffix(scope, "/") {
|
||||
scope = scope + "/"
|
||||
}
|
||||
grantedAccessList := make([]auth.Access, 0, len(requestedAccessList))
|
||||
for _, access := range requestedAccessList {
|
||||
if access.Type != "repository" {
|
||||
context.GetLogger(ctx).Debugf("Skipping unsupported resource type: %s", access.Type)
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(access.Name, scope) {
|
||||
context.GetLogger(ctx).Debugf("Resource scope not allowed: %s", access.Name)
|
||||
continue
|
||||
}
|
||||
grantedAccessList = append(grantedAccessList, access)
|
||||
}
|
||||
return grantedAccessList
|
||||
}
|
||||
|
||||
// getToken handles authenticating the request and authorizing access to the
|
||||
|
@ -133,6 +184,15 @@ func (ts *tokenServer) getToken(ctx context.Context, w http.ResponseWriter, r *h
|
|||
params := r.URL.Query()
|
||||
service := params.Get("service")
|
||||
scopeSpecifiers := params["scope"]
|
||||
var offline bool
|
||||
if offlineStr := params.Get("offline_token"); offlineStr != "" {
|
||||
var err error
|
||||
offline, err = strconv.ParseBool(offlineStr)
|
||||
if err != nil {
|
||||
handleError(ctx, ErrorBadTokenOption.WithDetail(err), w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
requestedAccessList := ResolveScopeSpecifiers(ctx, scopeSpecifiers)
|
||||
|
||||
|
@ -166,20 +226,7 @@ func (ts *tokenServer) getToken(ctx context.Context, w http.ResponseWriter, r *h
|
|||
ctx = context.WithValue(ctx, "requestedAccess", requestedAccessList)
|
||||
ctx = context.WithLogger(ctx, context.GetLogger(ctx, "requestedAccess"))
|
||||
|
||||
scopePrefix := username + "/"
|
||||
grantedAccessList := make([]auth.Access, 0, len(requestedAccessList))
|
||||
for _, access := range requestedAccessList {
|
||||
if access.Type != "repository" {
|
||||
context.GetLogger(ctx).Debugf("Skipping unsupported resource type: %s", access.Type)
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(access.Name, scopePrefix) {
|
||||
context.GetLogger(ctx).Debugf("Resource scope not allowed: %s", access.Name)
|
||||
continue
|
||||
}
|
||||
grantedAccessList = append(grantedAccessList, access)
|
||||
}
|
||||
|
||||
grantedAccessList := filterAccessList(ctx, username, requestedAccessList)
|
||||
ctx = context.WithValue(ctx, "grantedAccess", grantedAccessList)
|
||||
ctx = context.WithLogger(ctx, context.GetLogger(ctx, "grantedAccess"))
|
||||
|
||||
|
@ -191,11 +238,151 @@ func (ts *tokenServer) getToken(ctx context.Context, w http.ResponseWriter, r *h
|
|||
|
||||
context.GetLogger(ctx).Info("authorized client")
|
||||
|
||||
// Get response context.
|
||||
response := tokenResponse{
|
||||
Token: token,
|
||||
ExpiresIn: int(ts.issuer.Expiration.Seconds()),
|
||||
}
|
||||
|
||||
if offline {
|
||||
response.RefreshToken = newRefreshToken()
|
||||
ts.refreshCache[response.RefreshToken] = refreshToken{
|
||||
subject: username,
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
ctx, w = context.WithResponseWriter(ctx, w)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"token": token})
|
||||
json.NewEncoder(w).Encode(response)
|
||||
|
||||
context.GetResponseLogger(ctx).Info("get token complete")
|
||||
}
|
||||
|
||||
type postTokenResponse struct {
|
||||
Token string `json:"access_token"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
ExpiresIn int `json:"expires_in,omitempty"`
|
||||
IssuedAt string `json:"issued_at,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
}
|
||||
|
||||
// postToken handles authenticating the request and authorizing access to the
|
||||
// requested scopes.
|
||||
func (ts *tokenServer) postToken(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
grantType := r.PostFormValue("grant_type")
|
||||
if grantType == "" {
|
||||
handleError(ctx, ErrorMissingRequiredField.WithDetail("missing grant_type value"), w)
|
||||
return
|
||||
}
|
||||
|
||||
service := r.PostFormValue("service")
|
||||
if service == "" {
|
||||
handleError(ctx, ErrorMissingRequiredField.WithDetail("missing service value"), w)
|
||||
return
|
||||
}
|
||||
|
||||
clientID := r.PostFormValue("client_id")
|
||||
if clientID == "" {
|
||||
handleError(ctx, ErrorMissingRequiredField.WithDetail("missing client_id value"), w)
|
||||
return
|
||||
}
|
||||
|
||||
var offline bool
|
||||
switch r.PostFormValue("access_type") {
|
||||
case "", "online":
|
||||
case "offline":
|
||||
offline = true
|
||||
default:
|
||||
handleError(ctx, ErrorUnsupportedValue.WithDetail("unknown access_type value"), w)
|
||||
return
|
||||
}
|
||||
|
||||
requestedAccessList := ResolveScopeList(ctx, r.PostFormValue("scope"))
|
||||
|
||||
var subject string
|
||||
var rToken string
|
||||
switch grantType {
|
||||
case "refresh_token":
|
||||
rToken = r.PostFormValue("refresh_token")
|
||||
if rToken == "" {
|
||||
handleError(ctx, ErrorUnsupportedValue.WithDetail("missing refresh_token value"), w)
|
||||
return
|
||||
}
|
||||
rt, ok := ts.refreshCache[rToken]
|
||||
if !ok || rt.service != service {
|
||||
handleError(ctx, errcode.ErrorCodeUnauthorized.WithDetail("invalid refresh token"), w)
|
||||
return
|
||||
}
|
||||
subject = rt.subject
|
||||
case "password":
|
||||
ca, ok := ts.accessController.(auth.CredentialAuthenticator)
|
||||
if !ok {
|
||||
handleError(ctx, ErrorUnsupportedValue.WithDetail("password grant type not supported"), w)
|
||||
return
|
||||
}
|
||||
subject = r.PostFormValue("username")
|
||||
if subject == "" {
|
||||
handleError(ctx, ErrorUnsupportedValue.WithDetail("missing username value"), w)
|
||||
return
|
||||
}
|
||||
password := r.PostFormValue("password")
|
||||
if password == "" {
|
||||
handleError(ctx, ErrorUnsupportedValue.WithDetail("missing password value"), w)
|
||||
return
|
||||
}
|
||||
if err := ca.AuthenticateUser(subject, password); err != nil {
|
||||
handleError(ctx, errcode.ErrorCodeUnauthorized.WithDetail("invalid credentials"), w)
|
||||
return
|
||||
}
|
||||
default:
|
||||
handleError(ctx, ErrorUnsupportedValue.WithDetail("unknown grant_type value"), w)
|
||||
return
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, "acctSubject", subject)
|
||||
ctx = context.WithLogger(ctx, context.GetLogger(ctx, "acctSubject"))
|
||||
|
||||
context.GetLogger(ctx).Info("authenticated client")
|
||||
|
||||
ctx = context.WithValue(ctx, "requestedAccess", requestedAccessList)
|
||||
ctx = context.WithLogger(ctx, context.GetLogger(ctx, "requestedAccess"))
|
||||
|
||||
grantedAccessList := filterAccessList(ctx, subject, requestedAccessList)
|
||||
ctx = context.WithValue(ctx, "grantedAccess", grantedAccessList)
|
||||
ctx = context.WithLogger(ctx, context.GetLogger(ctx, "grantedAccess"))
|
||||
|
||||
token, err := ts.issuer.CreateJWT(subject, service, grantedAccessList)
|
||||
if err != nil {
|
||||
handleError(ctx, err, w)
|
||||
return
|
||||
}
|
||||
|
||||
context.GetLogger(ctx).Info("authorized client")
|
||||
|
||||
response := postTokenResponse{
|
||||
Token: token,
|
||||
ExpiresIn: int(ts.issuer.Expiration.Seconds()),
|
||||
IssuedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
Scope: ToScopeList(grantedAccessList),
|
||||
}
|
||||
|
||||
if offline {
|
||||
rToken = newRefreshToken()
|
||||
ts.refreshCache[rToken] = refreshToken{
|
||||
subject: subject,
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
if rToken != "" {
|
||||
response.RefreshToken = rToken
|
||||
}
|
||||
|
||||
ctx, w = context.WithResponseWriter(ctx, w)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
|
||||
context.GetResponseLogger(ctx).Info("post token complete")
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue