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:
parent
8ee5607254
commit
9a7a65f05e
70
api/server/auth/auth.go
Normal file
70
api/server/auth/auth.go
Normal 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)
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
34
auth/provider/basic/basic.go
Normal file
34
auth/provider/basic/basic.go
Normal 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 ""
|
||||
}
|
42
auth/provider/oauth/oauth.go
Normal file
42
auth/provider/oauth/oauth.go
Normal 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
47
auth/provider/options.go
Normal 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
28
auth/provider/provider.go
Normal 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
|
||||
}
|
@ -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)
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user