github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/auth/token/token_provider.go (about)

     1  package token
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"time"
     8  
     9  	"github.com/qri-io/qri/auth/key"
    10  	"github.com/qri-io/qri/profile"
    11  )
    12  
    13  const (
    14  	// RTCode signals the token response type is 'code'
    15  	RTCode ResponseType = "code"
    16  	// RTToken signals the token response type is 'token'
    17  	RTToken ResponseType = "token"
    18  
    19  	// AccessTokenTTL is the lifespan of an access token
    20  	AccessTokenTTL = time.Hour * 2
    21  	// RefreshTokenTTL is the lifespan of a refresh token
    22  	RefreshTokenTTL = time.Hour * 24 * 30
    23  	// AccessCodeTTL is the lifespan of an access code
    24  	AccessCodeTTL = time.Minute * 2
    25  )
    26  
    27  var (
    28  	// ErrInvalidRequest is returned on any parse or void output error
    29  	ErrInvalidRequest = fmt.Errorf("invalid request")
    30  	// ErrInvalidCredentials signals a bad username/password/key error
    31  	ErrInvalidCredentials = fmt.Errorf("invalid user credentials")
    32  	// ErrNotFound is returned when no matching results exist for the provided credentials
    33  	ErrNotFound = fmt.Errorf("user not found")
    34  	// ErrServerError is returned on unexpected errors
    35  	ErrServerError = fmt.Errorf("server error")
    36  	// ErrInvalidAuthorizeCode is returned on parsing an invalid authorization code
    37  	ErrInvalidAuthorizeCode = fmt.Errorf("invalid authorize code")
    38  	// ErrInvalidAccessToken is returned on parsing an invalid access token
    39  	ErrInvalidAccessToken = fmt.Errorf("invalid access token")
    40  	// ErrCodeExpired is returned for expired authorization codes
    41  	ErrCodeExpired = fmt.Errorf("code expired")
    42  	// ErrTokenExpired is returned for expired tokens
    43  	ErrTokenExpired = fmt.Errorf("token expired")
    44  	// ErrInvalidRefreshToken is returned on parsing invalid refresh tokens
    45  	ErrInvalidRefreshToken = fmt.Errorf("invalid refresh token")
    46  )
    47  
    48  // Provider is a service that generates access & refresh tokens
    49  type Provider interface {
    50  	// Token handles the auth token flow
    51  	Token(ctx context.Context, req *Request) (*Response, error)
    52  }
    53  
    54  // Request is a wrapper for incoming token requests
    55  type Request struct {
    56  	GrantType    GrantType `json:"grant_type"`
    57  	Code         string    `json:"code"`
    58  	Username     string    `json:"username"`
    59  	Password     string    `json:"password"`
    60  	RefreshToken string    `json:"refresh_token"`
    61  	RedirectURI  string    `json:"redirect_uri"`
    62  }
    63  
    64  // Response wraps the token response object
    65  type Response struct {
    66  	AccessToken  string `json:"access_token"`
    67  	TokenType    string `json:"token_type"`
    68  	ExpiresIn    int64  `json:"expires_in"`
    69  	RefreshToken string `json:"refresh_token,omitempty"`
    70  }
    71  
    72  // ResponseType the type of authorization request
    73  type ResponseType string
    74  
    75  func (rt ResponseType) String() string {
    76  	return string(rt)
    77  }
    78  
    79  // GrantType authorization model
    80  type GrantType string
    81  
    82  // define authorization model
    83  const (
    84  	AuthorizationCode   GrantType = "authorization_code"
    85  	PasswordCredentials GrantType = "password"
    86  	ClientCredentials   GrantType = "client_credentials"
    87  	Refreshing          GrantType = "refresh_token"
    88  	Implicit            GrantType = "__implicit"
    89  )
    90  
    91  func (gt GrantType) String() string {
    92  	if gt == AuthorizationCode ||
    93  		gt == PasswordCredentials ||
    94  		gt == ClientCredentials ||
    95  		gt == Refreshing {
    96  		return string(gt)
    97  	}
    98  	return ""
    99  }
   100  
   101  // ClientType is used to enumerate the user types to distingish them later from the token
   102  type ClientType string
   103  
   104  const (
   105  	// UserClient represents a human user that's authenticated with his own credentials
   106  	UserClient ClientType = "user"
   107  	// NodeClient represents a machine client that's authenticated with api client credentials
   108  	NodeClient ClientType = "node"
   109  )
   110  
   111  func (ct ClientType) String() string {
   112  	return string(ct)
   113  }
   114  
   115  // LocalProvider implements the Provider interface and
   116  // provides mechanics for generating tokens for a selected profile
   117  type LocalProvider struct {
   118  	profiles profile.Store
   119  	keys     key.Store
   120  }
   121  
   122  // NewProvider instantiates a new LocalProvider
   123  func NewProvider(p profile.Store, k key.Store) (*LocalProvider, error) {
   124  	return &LocalProvider{
   125  		profiles: p,
   126  		keys:     k,
   127  	}, nil
   128  }
   129  
   130  // compile-time assertion that LocalProvider is a token.Provider
   131  var _ Provider = (*LocalProvider)(nil)
   132  
   133  // Token handles the OAuth token flow
   134  func (p *LocalProvider) Token(ctx context.Context, req *Request) (*Response, error) {
   135  	log.Debugf("token.Provider got request: %+v", req)
   136  	resp := &Response{TokenType: "jwt", ExpiresIn: int64(AccessTokenTTL.Seconds())}
   137  	switch req.GrantType {
   138  	case PasswordCredentials:
   139  		if req.Username == "" {
   140  			return nil, ErrInvalidCredentials
   141  		}
   142  		// TODO(arqu): this only selects the first returned profile for a given peername.
   143  		// ideally we would use the profile.ID to fetch the exact profile
   144  		// or otherwise validate the signatures
   145  		pros, err := p.profiles.ProfilesForUsername(ctx, req.Username)
   146  		if err != nil {
   147  			log.Debugf("token.Provider failed to fetch profiles: %q", err.Error())
   148  			return nil, ErrInvalidRequest
   149  		}
   150  		if len(pros) == 0 {
   151  			log.Debugf("token.Provider no matching profiles found")
   152  			return nil, ErrNotFound
   153  		}
   154  		if len(pros) > 1 {
   155  			log.Infof("token.Provider found multiple profiles for the given username - selected the first one")
   156  		}
   157  		pro := pros[0]
   158  		if pro.PrivKey == nil {
   159  			log.Debugf("token.Provider private key is nil")
   160  			return nil, ErrInvalidCredentials
   161  		}
   162  		accessToken, err := NewPrivKeyAuthToken(pro.PrivKey, pro.ID.Encode(), AccessTokenTTL)
   163  		if err != nil {
   164  			log.Debugf("token.Provider failed to generate access token: %q", err.Error())
   165  			return nil, ErrInvalidRequest
   166  		}
   167  		refreshToken, err := NewPrivKeyAuthToken(pro.PrivKey, pro.ID.Encode(), RefreshTokenTTL)
   168  		if err != nil {
   169  			log.Debugf("token.Provider failed to generate refresh token: %q", err.Error())
   170  			return nil, ErrInvalidRequest
   171  		}
   172  		resp.AccessToken = accessToken
   173  		resp.RefreshToken = refreshToken
   174  	case Refreshing:
   175  		if req.RefreshToken == "" {
   176  			return nil, ErrInvalidRequest
   177  		}
   178  		tok, err := ParseAuthToken(ctx, req.RefreshToken, p.keys)
   179  		if err != nil {
   180  			log.Debugf("token.Provider error parsing refresh token: %q", err.Error())
   181  			return nil, ErrInvalidRequest
   182  		}
   183  
   184  		if claims, ok := tok.Claims.(*Claims); ok {
   185  			pid, err := profile.IDB58Decode(claims.Subject)
   186  			if err != nil {
   187  				log.Debugf("token.Provider failed to parse profileID")
   188  				return nil, ErrInvalidRequest
   189  			}
   190  			pro, err := p.profiles.GetProfile(ctx, pid)
   191  			if errors.Is(err, profile.ErrNotFound) {
   192  				log.Debugf("token.Provider profile not found")
   193  				return nil, ErrNotFound
   194  			}
   195  			accessToken, err := NewPrivKeyAuthToken(pro.PrivKey, pro.ID.Encode(), AccessTokenTTL)
   196  			if err != nil {
   197  				log.Debugf("token.Provider failed to generate access token: %q", err.Error())
   198  				return nil, ErrInvalidRequest
   199  			}
   200  			resp.AccessToken = accessToken
   201  		}
   202  	default:
   203  		return nil, ErrInvalidRequest
   204  	}
   205  	return resp, nil
   206  }