Auth Provider (#1309)

* auth provider mock interface

* Auth Provider Options

* Implement API Server Auth Package

* Add weh utils

* Add Login URL

* Auth Provider Options

* Add auth provider scope and setting token in cookie

* Remove auth_login_url flag

Co-authored-by: Asim Aslam <asim@aslam.me>
Co-authored-by: Ben Toogood <ben@micro.mu>
This commit is contained in:
ben-toogood 2020-03-07 11:06:57 +00:00 committed by GitHub
parent 8ee5607254
commit 9a7a65f05e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 350 additions and 1 deletions

70
api/server/auth/auth.go Normal file
View File

@ -0,0 +1,70 @@
package auth
import (
"net/http"
"strings"
"github.com/micro/go-micro/v2/auth"
"github.com/micro/go-micro/v2/metadata"
)
// CombinedAuthHandler wraps a server and authenticates requests
func CombinedAuthHandler(h http.Handler) http.Handler {
return authHandler{
handler: h,
auth: auth.DefaultAuth,
}
}
type authHandler struct {
handler http.Handler
auth auth.Auth
}
const (
// BearerScheme is the prefix in the auth header
BearerScheme = "Bearer "
)
func (h authHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
loginURL := h.auth.Options().LoginURL
// Return if the user disabled auth on this endpoint
excludes := h.auth.Options().Exclude
if len(loginURL) > 0 {
excludes = append(excludes, loginURL)
}
for _, e := range excludes {
if e == req.URL.Path {
h.handler.ServeHTTP(w, req)
return
}
}
var token string
if header, ok := metadata.Get(req.Context(), "Authorization"); ok {
// Extract the auth token from the request
if strings.HasPrefix(header, BearerScheme) {
token = header[len(BearerScheme):]
}
} else {
// Get the token out the cookies if not provided in headers
if c, err := req.Cookie(auth.CookieName); err != nil && c != nil {
token = c.Value
}
}
// If the token is valid, allow the request
if _, err := h.auth.Verify(token); err == nil {
h.handler.ServeHTTP(w, req)
return
}
// If there is no auth login url set, 401
if loginURL == "" {
w.WriteHeader(401)
}
// Redirect to the login path
http.Redirect(w, req, loginURL, http.StatusTemporaryRedirect)
}

View File

@ -8,6 +8,8 @@ import (
"os"
"sync"
"github.com/micro/go-micro/v2/api/server/auth"
"github.com/gorilla/handlers"
"github.com/micro/go-micro/v2/api/server"
"github.com/micro/go-micro/v2/api/server/cors"
@ -47,6 +49,7 @@ func (s *httpServer) Init(opts ...server.Option) error {
func (s *httpServer) Handle(path string, handler http.Handler) {
h := handlers.CombinedLoggingHandler(os.Stdout, handler)
h = auth.CombinedAuthHandler(handler)
if s.opts.EnableCORS {
h = cors.CombinedCORSHandler(h)

View File

@ -44,7 +44,7 @@ type Role struct {
// Account provided by an auth provider
type Account struct {
// ID of the account (UUID or email)
// ID of the account (UUIDV4, email or username)
Id string `json:"id"`
// Token used to authenticate
Token string `json:"token"`
@ -62,6 +62,9 @@ const (
// MetadataKey is the key used when storing the account
// in metadata
MetadataKey = "auth-account"
// CookieName is the name of the cookie which stores the
// auth token
CookieName = "micro-token"
)
// AccountFromContext gets the account from the context, which

View File

@ -1,5 +1,7 @@
package auth
import "github.com/micro/go-micro/v2/auth/provider"
type Options struct {
// Token is an auth token
Token string
@ -9,6 +11,10 @@ type Options struct {
PrivateKey string
// Endpoints to exclude
Exclude []string
// Provider is an auth provider
Provider provider.Provider
// LoginURL is the relative url path where a user can login
LoginURL string
}
type Option func(o *Options)
@ -41,6 +47,20 @@ func Token(t string) Option {
}
}
// Provider set the auth provider
func Provider(p provider.Provider) Option {
return func(o *Options) {
o.Provider = p
}
}
// LoginURL sets the auth LoginURL
func LoginURL(url string) Option {
return func(o *Options) {
o.LoginURL = url
}
}
type GenerateOptions struct {
// Metadata associated with the account
Metadata map[string]string

View File

@ -0,0 +1,34 @@
package basic
import (
"github.com/micro/go-micro/v2/auth/provider"
)
// NewProvider returns an initialised basic provider
func NewProvider(opts ...provider.Option) provider.Provider {
var options provider.Options
for _, o := range opts {
o(&options)
}
return &basic{options}
}
type basic struct {
opts provider.Options
}
func (b *basic) String() string {
return "basic"
}
func (b *basic) Options() provider.Options {
return b.opts
}
func (b *basic) Endpoint() string {
return ""
}
func (b *basic) Redirect() string {
return ""
}

View File

@ -0,0 +1,42 @@
package oauth
import (
"fmt"
"github.com/micro/go-micro/v2/auth/provider"
)
// NewProvider returns an initialised oauth provider
func NewProvider(opts ...provider.Option) provider.Provider {
var options provider.Options
for _, o := range opts {
o(&options)
}
return &oauth{options}
}
type oauth struct {
opts provider.Options
}
func (o *oauth) String() string {
return "oauth"
}
func (o *oauth) Options() provider.Options {
return o.opts
}
func (o *oauth) Endpoint() string {
s := fmt.Sprintf("%v?client_id=%v", o.opts.Endpoint, o.opts.ClientID)
if scope := o.opts.Scope; len(scope) > 0 {
s = fmt.Sprintf("%v&scope=%v", s, scope)
}
return s
}
func (o *oauth) Redirect() string {
return o.opts.Redirect
}

47
auth/provider/options.go Normal file
View File

@ -0,0 +1,47 @@
package provider
// Option returns a function which sets an option
type Option func(*Options)
// Options a provider can have
type Options struct {
// ClientID is the application's ID.
ClientID string
// ClientSecret is the application's secret.
ClientSecret string
// Endpoint for the provider
Endpoint string
// Redirect url incase of UI
Redirect string
// Scope of the oauth request
Scope string
}
// Credentials is an option which sets the client id and secret
func Credentials(id, secret string) Option {
return func(o *Options) {
o.ClientID = id
o.ClientSecret = secret
}
}
// Endpoint sets the endpoint option
func Endpoint(e string) Option {
return func(o *Options) {
o.Endpoint = e
}
}
// Redirect sets the Redirect option
func Redirect(r string) Option {
return func(o *Options) {
o.Redirect = r
}
}
// Scope sets the oauth scope
func Scope(s string) Option {
return func(o *Options) {
o.Scope = s
}
}

28
auth/provider/provider.go Normal file
View File

@ -0,0 +1,28 @@
// Package provider is an external auth provider e.g oauth
package provider
import (
"time"
)
// Provider is an auth provider
type Provider interface {
// String returns the name of the provider
String() string
// Options returns the options of a provider
Options() Options
// Endpoint for the provider
Endpoint() string
// Redirect url incase of UI
Redirect() string
}
// Grant is a granted authorisation
type Grant struct {
// token for reuse
Token string
// Expiry of the token
Expiry time.Time
// Scopes associated with grant
Scopes []string
}

View File

@ -7,6 +7,8 @@ import (
"strings"
"time"
"github.com/micro/go-micro/v2/auth/provider"
"github.com/micro/go-micro/v2/auth"
"github.com/micro/go-micro/v2/broker"
"github.com/micro/go-micro/v2/client"
@ -70,6 +72,10 @@ import (
jwtAuth "github.com/micro/go-micro/v2/auth/jwt"
sAuth "github.com/micro/go-micro/v2/auth/service"
storeAuth "github.com/micro/go-micro/v2/auth/store"
// auth providers
"github.com/micro/go-micro/v2/auth/provider/basic"
"github.com/micro/go-micro/v2/auth/provider/oauth"
)
type Cmd interface {
@ -269,6 +275,36 @@ var (
EnvVars: []string{"MICRO_AUTH_EXCLUDE"},
Usage: "Comma-separated list of endpoints excluded from authentication, e.g. Users.ListUsers",
},
&cli.StringFlag{
Name: "auth_provider",
EnvVars: []string{"MICRO_AUTH_PROVIDER"},
Usage: "Auth provider used to login user",
},
&cli.StringFlag{
Name: "auth_provider_client_id",
EnvVars: []string{"MICRO_AUTH_PROVIDER_CLIENT_ID"},
Usage: "The client id to be used for oauth",
},
&cli.StringFlag{
Name: "auth_provider_client_secret",
EnvVars: []string{"MICRO_AUTH_PROVIDER_CLIENT_SECRET"},
Usage: "The client secret to be used for oauth",
},
&cli.StringFlag{
Name: "auth_provider_endpoint",
EnvVars: []string{"MICRO_AUTH_PROVIDER_ENDPOINT"},
Usage: "The enpoint to be used for oauth",
},
&cli.StringFlag{
Name: "auth_provider_redirect",
EnvVars: []string{"MICRO_AUTH_PROVIDER_REDIRECT"},
Usage: "The redirect to be used for oauth",
},
&cli.StringFlag{
Name: "auth_provider_scope",
EnvVars: []string{"MICRO_AUTH_PROVIDER_SCOPE"},
Usage: "The scope to be used for oauth",
},
}
DefaultBrokers = map[string]func(...broker.Option) broker.Broker{
@ -328,6 +364,11 @@ var (
"jwt": jwtAuth.NewAuth,
}
DefaultAuthProviders = map[string]func(...provider.Option) provider.Provider{
"oauth": oauth.NewProvider,
"basic": basic.NewProvider,
}
DefaultProfiles = map[string]func(...profile.Option) profile.Profile{
"http": http.NewProfile,
"pprof": pprof.NewProfile,
@ -627,6 +668,32 @@ func (c *cmd) Before(ctx *cli.Context) error {
authOpts = append(authOpts, auth.Exclude(ctx.StringSlice("auth_exclude")...))
}
if name := ctx.String("auth_provider"); len(name) > 0 {
p, ok := DefaultAuthProviders[name]
if !ok {
return fmt.Errorf("AuthProvider %s not found", name)
}
var provOpts []provider.Option
clientID := ctx.String("auth_provider_client_id")
clientSecret := ctx.String("auth_provider_client_secret")
if len(clientID) > 0 || len(clientSecret) > 0 {
provOpts = append(provOpts, provider.Credentials(clientID, clientSecret))
}
if e := ctx.String("auth_provider_endpoint"); len(e) > 0 {
provOpts = append(provOpts, provider.Endpoint(e))
}
if r := ctx.String("auth_provider_redirect"); len(r) > 0 {
provOpts = append(provOpts, provider.Redirect(r))
}
if s := ctx.String("auth_provider_scope"); len(s) > 0 {
provOpts = append(provOpts, provider.Scope(s))
}
authOpts = append(authOpts, auth.Provider(p(provOpts...)))
}
if len(authOpts) > 0 {
if err := (*c.opts.Auth).Init(authOpts...); err != nil {
log.Fatalf("Error configuring auth: %v", err)

View File

@ -1,12 +1,47 @@
package http
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/micro/go-micro/v2/client/selector"
"github.com/micro/go-micro/v2/registry"
)
// Write sets the status and body on a http ResponseWriter
func Write(w http.ResponseWriter, contentType string, status int, body string) {
w.Header().Set("Content-Length", fmt.Sprintf("%v", len(body)))
w.Header().Set("Content-Type", contentType)
w.WriteHeader(status)
fmt.Fprintf(w, `%v`, body)
}
// WriteBadRequestError sets a 400 status code
func WriteBadRequestError(w http.ResponseWriter, err error) {
rawBody, err := json.Marshal(map[string]string{
"error": err.Error(),
})
if err != nil {
WriteInternalServerError(w, err)
return
}
Write(w, "application/json", 400, string(rawBody))
}
// WriteInternalServerError sets a 500 status code
func WriteInternalServerError(w http.ResponseWriter, err error) {
rawBody, err := json.Marshal(map[string]string{
"error": err.Error(),
})
if err != nil {
log.Println(err)
return
}
Write(w, "application/json", 500, string(rawBody))
}
func NewRoundTripper(opts ...Option) http.RoundTripper {
options := Options{
Registry: registry.DefaultRegistry,