#819 add SendJWTHeader auth option, expose it on /config
This commit is contained in:
committed by
Umputun
parent
5b4e492c1b
commit
6426ea154e
@@ -79,8 +79,9 @@ type ServerCommand struct {
|
||||
|
||||
Auth struct {
|
||||
TTL struct {
|
||||
JWT time.Duration `long:"jwt" env:"JWT" default:"5m" description:"jwt TTL"`
|
||||
Cookie time.Duration `long:"cookie" env:"COOKIE" default:"200h" description:"auth cookie TTL"`
|
||||
JWT time.Duration `long:"jwt" env:"JWT" default:"5m" description:"jwt TTL"`
|
||||
SendJWTHeader bool `long:"send-jwt-header" env:"SEND_JWT_HEADER" description:"send JWT as a header instead of cookie"`
|
||||
Cookie time.Duration `long:"cookie" env:"COOKIE" default:"200h" description:"auth cookie TTL"`
|
||||
} `group:"ttl" namespace:"ttl" env-namespace:"TTL"`
|
||||
Google AuthGroup `group:"google" namespace:"google" env-namespace:"GOOGLE" description:"Google OAuth"`
|
||||
Github AuthGroup `group:"github" namespace:"github" env-namespace:"GITHUB" description:"Github OAuth"`
|
||||
@@ -455,6 +456,7 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) {
|
||||
AnonVote: s.AnonymousVote && s.RestrictVoteIP,
|
||||
SimpleView: s.SimpleView,
|
||||
ProxyCORS: s.ProxyCORS,
|
||||
SendJWTHeader: s.Auth.TTL.SendJWTHeader,
|
||||
}
|
||||
|
||||
srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = s.LowScore, s.CriticalScore
|
||||
@@ -908,6 +910,7 @@ func (s *ServerCommand) makeAuthenticator(ds *service.DataStore, avas avatar.Sto
|
||||
URL: strings.TrimSuffix(s.RemarkURL, "/"),
|
||||
Issuer: "remark42",
|
||||
TokenDuration: s.Auth.TTL.JWT,
|
||||
SendJWTHeader: s.Auth.TTL.SendJWTHeader,
|
||||
CookieDuration: s.Auth.TTL.Cookie,
|
||||
SecureCookies: strings.HasPrefix(s.RemarkURL, "https://"),
|
||||
SecretReader: token.SecretFunc(func(aud string) (string, error) { // get secret per site
|
||||
|
||||
@@ -62,6 +62,7 @@ type Rest struct {
|
||||
EmojiEnabled bool
|
||||
SimpleView bool
|
||||
ProxyCORS bool
|
||||
SendJWTHeader bool
|
||||
|
||||
SSLConfig SSLConfig
|
||||
httpsServer *http.Server
|
||||
@@ -422,6 +423,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
EmailNotifications bool `json:"email_notifications"`
|
||||
EmojiEnabled bool `json:"emoji_enabled"`
|
||||
SimpleView bool `json:"simple_view"`
|
||||
SendJWTHeader bool `json:"send_jwt_header"`
|
||||
}{
|
||||
Version: s.Version,
|
||||
EditDuration: int(s.DataService.EditDuration.Seconds()),
|
||||
@@ -437,6 +439,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
EmojiEnabled: s.EmojiEnabled,
|
||||
AnonVote: s.AnonVote,
|
||||
SimpleView: s.SimpleView,
|
||||
SendJWTHeader: s.SendJWTHeader,
|
||||
}
|
||||
|
||||
cnf.Auth = []string{}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ require (
|
||||
github.com/go-chi/chi v4.1.1+incompatible
|
||||
github.com/go-chi/cors v1.1.1
|
||||
github.com/go-chi/render v1.0.1
|
||||
github.com/go-pkgz/auth v0.11.0
|
||||
github.com/go-pkgz/auth v1.13.0
|
||||
github.com/go-pkgz/jrpc v0.2.0
|
||||
github.com/go-pkgz/lcw v0.7.1
|
||||
github.com/go-pkgz/lgr v0.7.0
|
||||
|
||||
@@ -58,6 +58,12 @@ github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
|
||||
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
|
||||
github.com/go-pkgz/auth v0.11.0 h1:xi9Y0KGZUftVLx/8tGdvIfUk/+/4QLBVL6k9A8C4ycM=
|
||||
github.com/go-pkgz/auth v0.11.0/go.mod h1:NzVqlTW0E9JXVdAaWRq81XZjICgHnNaNdUfE3CbS2T4=
|
||||
github.com/go-pkgz/auth v0.12.1 h1:tLdxRnK444PUZK4AGgTc8GJ4MxX4cEIIv7BMlpNLySk=
|
||||
github.com/go-pkgz/auth v0.12.1/go.mod h1:NzVqlTW0E9JXVdAaWRq81XZjICgHnNaNdUfE3CbS2T4=
|
||||
github.com/go-pkgz/auth v0.12.2-0.20201016021254-bc202515c232 h1:CtzGRs1LY06CWBEj7TSiOcH7M6vKHB7DBgCSy9/3QdU=
|
||||
github.com/go-pkgz/auth v0.12.2-0.20201016021254-bc202515c232/go.mod h1:+8DMssa9T0C75rvwfsgNXatOfweaMF8UK9n1dgPLLiU=
|
||||
github.com/go-pkgz/auth v1.13.0 h1:CzCYmf9zgfTPEVRL3mEebd5g281QF3+0+wtLpZMxBvU=
|
||||
github.com/go-pkgz/auth v1.13.0/go.mod h1:+8DMssa9T0C75rvwfsgNXatOfweaMF8UK9n1dgPLLiU=
|
||||
github.com/go-pkgz/expirable-cache v0.0.3 h1:rTh6qNPp78z0bQE6HDhXBHUwqnV9i09Vm6dksJLXQDc=
|
||||
github.com/go-pkgz/expirable-cache v0.0.3/go.mod h1:+IauqN00R2FqNRLCLA+X5YljQJrwB179PfiAoMPlTlQ=
|
||||
github.com/go-pkgz/jrpc v0.2.0 h1:CLy/eZyekjraVrxZV18N2R1mYLMJ/nWrgdfyIOGPY/E=
|
||||
|
||||
+92
-9
@@ -1,7 +1,7 @@
|
||||
# auth - authentication via oauth2, direct and email
|
||||
[](https://github.com/go-pkgz/auth/actions) [](https://coveralls.io/github/go-pkgz/auth?branch=master) [](https://pkg.go.dev/github.com/go-pkgz/auth?tab=doc)
|
||||
|
||||
This library provides "social login" with Github, Google, Facebook, Microsoft, Twitter, Yandex and Battle.net as well as custom auth providers and email verification.
|
||||
This library provides "social login" with Github, Google, Facebook, Microsoft, Twitter, Yandex, Battle.net and Telegram as well as custom auth providers and email verification.
|
||||
|
||||
- Multiple oauth2 providers can be used at the same time
|
||||
- Special `dev` provider allows local testing and development
|
||||
@@ -20,6 +20,7 @@ This library provides "social login" with Github, Google, Facebook, Microsoft, T
|
||||
- Pre-auth and post-auth hooks to handle custom use cases.
|
||||
- Middleware for easy integration into http routers
|
||||
- Wrappers to extract user info from the request
|
||||
- Role based access control
|
||||
|
||||
## Install
|
||||
|
||||
@@ -77,6 +78,7 @@ func main() {
|
||||
- `middleware.Auth` - requires authenticated user
|
||||
- `middleware.Admin` - requires authenticated admin user
|
||||
- `middleware.Trace` - doesn't require authenticated user, but adds user info to request
|
||||
- `middleware.RBAC` - requires authenticated user with passed role(s)
|
||||
|
||||
Also, there is a special middleware `middleware.UpdateUser` for population and modifying UserInfo in every request. See "Customization" for more details.
|
||||
|
||||
@@ -86,7 +88,7 @@ Generally, adding support of `auth` includes a few relatively simple steps:
|
||||
|
||||
1. Setup `auth.Opts` structure with all parameters. Each of them [documented](https://github.com/go-pkgz/auth/blob/master/auth.go#L29) and most of parameters are optional and have sane defaults.
|
||||
2. [Create](https://github.com/go-pkgz/auth/blob/master/auth.go#L56) the new `auth.Service` with provided options.
|
||||
3. [Add all](https://github.com/go-pkgz/auth/blob/master/auth.go#L149) desirable authentication providers. Currently supported Github, Google, Facebook and Yandex
|
||||
3. [Add all](https://github.com/go-pkgz/auth/blob/master/auth.go#L149) desirable authentication providers.
|
||||
4. Retrieve [middleware](https://github.com/go-pkgz/auth/blob/master/auth.go#L144) and [http handlers](https://github.com/go-pkgz/auth/blob/master/auth.go#L105) from `auth.Service`
|
||||
5. Wire auth and avatar handlers into http router as sub–routes.
|
||||
|
||||
@@ -148,7 +150,27 @@ In addition to oauth2 providers `auth.Service` allows to use direct user-defined
|
||||
|
||||
Such provider acts like any other, i.e. will be registered as `/auth/local/login`.
|
||||
|
||||
The API for this provider - `GET /auth/<name>/login?user=<user>&passwd=<password>&aud=<site_id>&session=[1|0]`
|
||||
The API for this provider supports both GET and POST requests:
|
||||
|
||||
* GET request with user credentials provided as query params:
|
||||
```
|
||||
GET /auth/<name>/login?user=<user>&passwd=<password>&aud=<site_id>&session=[1|0]
|
||||
```
|
||||
* POST request could be encoded as application/x-www-form-urlencoded or application/json:
|
||||
```
|
||||
POST /auth/<name>/login?session=[1|0]
|
||||
body: application/x-www-form-urlencoded
|
||||
user=<user>&passwd=<password>&aud=<site_id>
|
||||
```
|
||||
```
|
||||
POST /auth/<name>/login?session=[1|0]
|
||||
body: application/json
|
||||
{
|
||||
"user": "name",
|
||||
"passwd": "xyz",
|
||||
"aud": "bar",
|
||||
}
|
||||
```
|
||||
|
||||
_note: password parameter doesn't have to be naked/real password and can be any kind of password hash prepared by caller._
|
||||
|
||||
@@ -187,6 +209,57 @@ The API for this provider:
|
||||
|
||||
The provider acts like any other, i.e. will be registered as `/auth/email/login`.
|
||||
|
||||
### Telegram
|
||||
|
||||
Telegram provider allows your users to log in with Telegram account. First, you will need to create your bot.
|
||||
Contact [@BotFather](https://t.me/botfather) and follow his instructions to create your own bot (call it, for example, "My site auth bot")
|
||||
|
||||
Next initialize TelegramHandler with following parameters:
|
||||
* `ProviderName` - Any unique name to distinguish between providers
|
||||
* `SuccessMsg` - Message sent to user on successfull authentication
|
||||
* `ErrorMsg` - Message sent on errors (e.g. login request expired)
|
||||
* `Telegram` - Telegram API implementation. Use provider.NewTelegramAPI with following arguments
|
||||
1. The secret token bot father gave you
|
||||
2. An http.Client for accessing Telegram API's
|
||||
|
||||
```go
|
||||
token := os.Getenv("TELEGRAM_TOKEN")
|
||||
|
||||
telegram := provider.TelegramHandler{
|
||||
ProviderName: "telegram",
|
||||
ErrorMsg: "❌ Invalid auth request. Please try clicking link again.",
|
||||
SuccessMsg: "✅ You have successfully authenticated!",
|
||||
Telegram: provider.NewTelegramAPI(token, http.DefaultClient),
|
||||
|
||||
L: log.Default(),
|
||||
TokenService: service.TokenService(),
|
||||
AvatarSaver: service.AvatarProxy(),
|
||||
}
|
||||
```
|
||||
|
||||
After that run provider and register it's handlers:
|
||||
```go
|
||||
// Run Telegram provider in the background
|
||||
go func() {
|
||||
err := telegram.Run(context.Background())
|
||||
if err != nil {
|
||||
log.Fatalf("[PANIC] failed to start telegram: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Register Telegram provider
|
||||
service.AddCustomHandler(&telegram)
|
||||
```
|
||||
|
||||
Now all your users have to do is click one of the following links and press **start**
|
||||
`tg://resolve?domain=<botname>&start=<token>` or `https://t.me/<botname>/?start=<token>`
|
||||
|
||||
Use the following routes to interact with provider:
|
||||
1. `/auth/<providerName>/login` - Obtain auth token. Returns JSON object with `bot` (bot username) and `token` (token itself) fields.
|
||||
2. `/auth/<providerName>/login?token=<token>` - Check if auth request has been confirmed (i.e. user pressed start). Sets session cookie and returns user info on success, errors with 404 otherwise.
|
||||
|
||||
3. `/auth/<providerName>/logout` - Invalidate user session.
|
||||
|
||||
### Custom oauth2
|
||||
|
||||
This provider brings two extra functions:
|
||||
@@ -234,7 +307,7 @@ In order to add a new oauth2 provider following input is required:
|
||||
WithLoginPage: true,
|
||||
}
|
||||
prov := provider.NewCustomServer(srv, sopts)
|
||||
|
||||
|
||||
// Start server
|
||||
go prov.Run(context.Background())
|
||||
```
|
||||
@@ -246,12 +319,22 @@ In order to add a new oauth2 provider following input is required:
|
||||
service.AddCustomProvider("custom123", auth.Client{Cid: "cid", Csecret: "csecret"}, prov.HandlerOpt)
|
||||
```
|
||||
|
||||
### Self-implemented auth handler
|
||||
Additionally it is possible to implement own auth handler. It may be useful if auth provider does not conform to oauth standard. Self-implemented handler has to implement `provider.Provider` interface.
|
||||
```go
|
||||
// customHandler implements provider.Provider interface
|
||||
c := customHandler{}
|
||||
|
||||
// add customHandler to stack of auth handlers
|
||||
service.AddCustomHandler(c)
|
||||
```
|
||||
|
||||
### Customization
|
||||
|
||||
There are several ways to adjust functionality of the library:
|
||||
|
||||
1. `SecretReader` - interface with a single method `Get(aud string) string` to return the secret used for JWT signing and verification
|
||||
1. `ClaimsUpdater` - interface with `Update(claims Claims) Claims` method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status and so on.
|
||||
1. `ClaimsUpdater` - interface with `Update(claims Claims) Claims` method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status, roles and so on.
|
||||
1. `Validator` - interface with `Validate(token string, claims Claims) bool` method. This is post-token hook and will be called on **each request** wrapped with `Auth` middleware. This will be the place for special logic to reject some tokens or users.
|
||||
1. `UserUpdater` - interface with `Update(claims token.User) token.User` method. This method will be called on **each request** wrapped with `UpdateUser` middleware. This will be the place for special logic modify User Info in request context. [Example of usage.]((https://github.com/go-pkgz/auth/blob/master/_example/main.go#L148))
|
||||
|
||||
@@ -337,11 +420,11 @@ _instructions for google oauth2 setup borrowed from [oauth2_proxy](https://githu
|
||||
|
||||
#### Microsoft Auth Provider
|
||||
|
||||
1 .Register a new application [using the Azure portal](https://docs.microsoft.com/en-us/graph/auth-register-app-v2).
|
||||
2. Under **"Authentication/Platform configurations/Web"** enter the correct url constructed as domain + `/auth/microsoft/callback`. i.e. `https://example.mysite.com/auth/microsoft/callback`
|
||||
3. In "Overview" take note of the **Application (client) ID**
|
||||
1. Register a new application [using the Azure portal](https://docs.microsoft.com/en-us/graph/auth-register-app-v2).
|
||||
2. Under **"Authentication/Platform configurations/Web"** enter the correct url constructed as domain + `/auth/microsoft/callback`. i.e. `https://example.mysite.com/auth/microsoft/callback`
|
||||
3. In "Overview" take note of the **Application (client) ID**
|
||||
4. Choose the new project from the top right project dropdown (only if another project is selected)
|
||||
5. Select "Certificates & secrets" and click on "+ New Client Secret".
|
||||
5. Select "Certificates & secrets" and click on "+ New Client Secret".
|
||||
|
||||
|
||||
#### GitHub Auth Provider
|
||||
|
||||
+9
@@ -53,6 +53,8 @@ type Opts struct {
|
||||
XSRFHeaderKey string // default "X-XSRF-TOKEN"
|
||||
JWTQuery string // default "token"
|
||||
|
||||
SendJWTHeader bool // if enabled send JWT as a header instead of cookie
|
||||
|
||||
Issuer string // optional value for iss claim, usually the application name, default "go-pkgz/auth"
|
||||
|
||||
URL string // root url for the rest service, i.e. http://blah.example.com, required
|
||||
@@ -105,6 +107,7 @@ func NewService(opts Opts) (res *Service) {
|
||||
JWTHeaderKey: opts.JWTHeaderKey,
|
||||
XSRFCookieName: opts.XSRFCookieName,
|
||||
XSRFHeaderKey: opts.XSRFHeaderKey,
|
||||
SendJWTHeader: opts.SendJWTHeader,
|
||||
JWTQuery: opts.JWTQuery,
|
||||
Issuer: res.issuer,
|
||||
AudienceReader: opts.AudienceReader,
|
||||
@@ -284,6 +287,12 @@ func (s *Service) AddVerifProvider(name, msgTmpl string, sender provider.Sender)
|
||||
s.authMiddleware.Providers = s.providers
|
||||
}
|
||||
|
||||
// AddCustomHandler adds user-defined self-implemented handler of auth provider
|
||||
func (s *Service) AddCustomHandler(handler provider.Provider) {
|
||||
s.providers = append(s.providers, provider.NewService(handler))
|
||||
s.authMiddleware.Providers = s.providers
|
||||
}
|
||||
|
||||
// DevAuth makes dev oauth2 server, for testing and development only!
|
||||
func (s *Service) DevAuth() (*provider.DevAuthServer, error) {
|
||||
p, err := s.Provider("dev") // peak dev provider
|
||||
|
||||
+1
@@ -5,6 +5,7 @@ go 1.14
|
||||
require (
|
||||
github.com/dghubble/oauth1 v0.6.0
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/go-pkgz/repeater v1.1.3
|
||||
github.com/go-pkgz/rest v1.5.0
|
||||
github.com/microcosm-cc/bluemonday v1.0.2
|
||||
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022
|
||||
|
||||
+2
@@ -18,6 +18,8 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||
github.com/go-pkgz/repeater v1.1.3 h1:q6+JQF14ESSy28Dd7F+wRelY4F+41HJ0LEy/szNnMiE=
|
||||
github.com/go-pkgz/repeater v1.1.3/go.mod h1:hVTavuO5x3Gxnu8zW7d6sQBfAneKV8X2FjU48kGfpKw=
|
||||
github.com/go-pkgz/rest v1.5.0 h1:C8SxXcXza4GiUUAn/95iCkvoIrGbS30qpwK19iqlrWQ=
|
||||
github.com/go-pkgz/rest v1.5.0/go.mod h1:nQaM3RhSTUAmbBZWY4hfe4buyeC9VckvhoCktiQXJxI=
|
||||
github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0=
|
||||
|
||||
+31
@@ -7,6 +7,7 @@ package middleware
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
@@ -187,3 +188,33 @@ func (a *Authenticator) basicAdminUser(r *http.Request) bool {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// RBAC middleware allows role based control for routes
|
||||
// this handler internally wrapped with auth(true) to avoid situation if RBAC defined without prior Auth
|
||||
func (a *Authenticator) RBAC(roles ...string) func(http.Handler) http.Handler {
|
||||
|
||||
f := func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := token.GetUserInfo(r)
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var matched bool
|
||||
for _, role := range roles {
|
||||
if strings.EqualFold(role, user.Role) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
http.Error(w, "Access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
return a.auth(true)(http.HandlerFunc(fn)) // enforce auth
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ func (f UserUpdFunc) Update(user token.User) token.User {
|
||||
}
|
||||
|
||||
// UpdateUser update user info with UserUpdater if it exists in request's context. Otherwise do nothing.
|
||||
// should be places after either Auth, Trace or AdminOnly middleware.
|
||||
// should be placed after either Auth, Trace. AdminOnly or RBAC middleware.
|
||||
func (a *Authenticator) UpdateUser(upd UserUpdater) func(http.Handler) http.Handler {
|
||||
f := func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
+2
@@ -78,11 +78,13 @@ func (c *CustomServer) Run(ctx context.Context) {
|
||||
u, err := url.Parse(c.URL)
|
||||
if err != nil {
|
||||
c.Logf("[ERROR] failed to parse service base URL=%s", c.URL)
|
||||
return
|
||||
}
|
||||
|
||||
_, port, err := net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
c.Logf("[ERROR] failed to get port from URL=%s", c.URL)
|
||||
return
|
||||
}
|
||||
|
||||
c.httpServer = &http.Server{
|
||||
|
||||
+73
-9
@@ -2,17 +2,24 @@ package provider
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"errors"
|
||||
"encoding/json"
|
||||
"mime"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/go-pkgz/rest"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/go-pkgz/auth/logger"
|
||||
"github.com/go-pkgz/auth/token"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxHTTPBodySize defines max http body size
|
||||
MaxHTTPBodySize = 1024 * 1024
|
||||
)
|
||||
|
||||
// DirectHandler implements non-oauth2 provider authorizing user in traditional way with storage
|
||||
// with users and hashes
|
||||
type DirectHandler struct {
|
||||
@@ -37,21 +44,45 @@ func (f CredCheckerFunc) Check(user, password string) (ok bool, err error) {
|
||||
return f(user, password)
|
||||
}
|
||||
|
||||
// credentials holds user credentials
|
||||
type credentials struct {
|
||||
User string `json:"user"`
|
||||
Password string `json:"passwd"`
|
||||
Audience string `json:"aud"`
|
||||
}
|
||||
|
||||
// Name of the handler
|
||||
func (p DirectHandler) Name() string { return p.ProviderName }
|
||||
|
||||
// LoginHandler checks "user" and "passwd" against data store and makes jwt if all passed
|
||||
// GET /something?user=name&password=xyz&sess=[0|1]
|
||||
// LoginHandler checks "user" and "passwd" against data store and makes jwt if all passed.
|
||||
//
|
||||
// GET /something?user=name&passwd=xyz&aud=bar&sess=[0|1]
|
||||
//
|
||||
// POST /something?sess[0|1]
|
||||
// Accepts application/x-www-form-urlencoded or application/json encoded requests.
|
||||
//
|
||||
// application/x-www-form-urlencoded body example:
|
||||
// user=name&passwd=xyz&aud=bar
|
||||
//
|
||||
// application/json body example:
|
||||
// {
|
||||
// "user": "name",
|
||||
// "passwd": "xyz",
|
||||
// "aud": "bar",
|
||||
// }
|
||||
func (p DirectHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
user, password := r.URL.Query().Get("user"), r.URL.Query().Get("passwd")
|
||||
aud := r.URL.Query().Get("aud")
|
||||
creds, err := p.getCredentials(w, r)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, p.L, http.StatusBadRequest, err, "failed to parse credentials")
|
||||
return
|
||||
}
|
||||
sessOnly := r.URL.Query().Get("sess") == "1"
|
||||
if p.CredChecker == nil {
|
||||
rest.SendErrorJSON(w, r, p.L, http.StatusInternalServerError,
|
||||
errors.New("no credential checker"), "no credential checker")
|
||||
return
|
||||
}
|
||||
ok, err := p.CredChecker.Check(user, password)
|
||||
ok, err := p.CredChecker.Check(creds.User, creds.Password)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, p.L, http.StatusInternalServerError, err, "failed to check user credentials")
|
||||
return
|
||||
@@ -61,8 +92,8 @@ func (p DirectHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
u := token.User{
|
||||
Name: user,
|
||||
ID: p.ProviderName + "_" + token.HashID(sha1.New(), user),
|
||||
Name: creds.User,
|
||||
ID: p.ProviderName + "_" + token.HashID(sha1.New(), creds.User),
|
||||
}
|
||||
u, err = setAvatar(p.AvatarSaver, u, &http.Client{Timeout: 5 * time.Second})
|
||||
if err != nil {
|
||||
@@ -81,7 +112,7 @@ func (p DirectHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Id: cid,
|
||||
Issuer: p.Issuer,
|
||||
Audience: aud,
|
||||
Audience: creds.Audience,
|
||||
},
|
||||
SessionOnly: sessOnly,
|
||||
}
|
||||
@@ -93,6 +124,39 @@ func (p DirectHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
rest.RenderJSON(w, r, claims.User)
|
||||
}
|
||||
|
||||
// getCredentials extracts user and password from request
|
||||
func (p DirectHandler) getCredentials(w http.ResponseWriter, r *http.Request) (credentials, error) {
|
||||
if r.Body != nil {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, MaxHTTPBodySize)
|
||||
}
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if contentType != "" {
|
||||
mt, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return credentials{}, err
|
||||
}
|
||||
contentType = mt
|
||||
}
|
||||
|
||||
if contentType == "application/json" {
|
||||
var creds credentials
|
||||
if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
|
||||
return credentials{}, errors.Wrap(err, "failed to parse request body")
|
||||
}
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return credentials{}, errors.Wrap(err, "failed to parse request")
|
||||
}
|
||||
|
||||
return credentials{
|
||||
User: r.Form.Get("user"),
|
||||
Password: r.Form.Get("passwd"),
|
||||
Audience: r.Form.Get("aud"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AuthHandler doesn't do anything for direct login as it has no callbacks
|
||||
func (p DirectHandler) AuthHandler(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ type Provider interface {
|
||||
// Handler returns auth routes for given provider
|
||||
func (p Service) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.Method != "GET" {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodPost {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
+415
@@ -0,0 +1,415 @@
|
||||
package provider
|
||||
|
||||
//go:generate moq -out telegram_moq_test.go . TelegramAPI
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
neturl "net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/go-pkgz/auth/logger"
|
||||
authtoken "github.com/go-pkgz/auth/token"
|
||||
"github.com/go-pkgz/repeater"
|
||||
"github.com/go-pkgz/rest"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// TelegramHandler implements login via telegram
|
||||
type TelegramHandler struct {
|
||||
logger.L
|
||||
|
||||
ProviderName string
|
||||
ErrorMsg, SuccessMsg string
|
||||
|
||||
TokenService TokenService
|
||||
AvatarSaver AvatarSaver
|
||||
Telegram TelegramAPI
|
||||
|
||||
username string // bot username
|
||||
requests struct {
|
||||
sync.RWMutex
|
||||
data map[string]tgAuthRequest
|
||||
}
|
||||
}
|
||||
|
||||
type tgAuthRequest struct {
|
||||
confirmed bool // whether login request has been confirmed and user info set
|
||||
expires time.Time
|
||||
user *authtoken.User
|
||||
}
|
||||
|
||||
// TelegramAPI is used for interacting with telegram API
|
||||
type TelegramAPI interface {
|
||||
GetUpdates(ctx context.Context) (*telegramUpdate, error)
|
||||
Avatar(ctx context.Context, userID int) (string, error)
|
||||
Send(ctx context.Context, id int, text string) error
|
||||
BotInfo(ctx context.Context) (*botInfo, error)
|
||||
}
|
||||
|
||||
// changed in tests
|
||||
var tgPollInterval = time.Second
|
||||
|
||||
// Run starts processing login requests sent in Telegram
|
||||
// Blocks caller
|
||||
func (th *TelegramHandler) Run(ctx context.Context) error {
|
||||
// Initialization
|
||||
info, err := th.Telegram.BotInfo(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to fetch bot info")
|
||||
}
|
||||
|
||||
th.requests.Lock()
|
||||
th.requests.data = make(map[string]tgAuthRequest)
|
||||
th.requests.Unlock()
|
||||
|
||||
th.username = info.Username
|
||||
|
||||
ticker := time.NewTicker(tgPollInterval)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
ticker.Stop()
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
err := th.processUpdates(ctx)
|
||||
if err != nil {
|
||||
th.Logf("Error while processing updates: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Purge expired requests
|
||||
now := time.Now()
|
||||
th.requests.Lock()
|
||||
for key, req := range th.requests.data {
|
||||
if now.After(req.expires) {
|
||||
delete(th.requests.data, key)
|
||||
}
|
||||
}
|
||||
th.requests.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type telegramUpdate struct {
|
||||
Result []struct {
|
||||
UpdateID int `json:"update_id"`
|
||||
Message struct {
|
||||
Chat struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"first_name"`
|
||||
Type string `json:"type"`
|
||||
} `json:"chat"`
|
||||
Text string `json:"text"`
|
||||
} `json:"message"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
// processUpdates processes a batch of updates from telegram servers
|
||||
// Returns offset for subsequent calls
|
||||
func (th *TelegramHandler) processUpdates(ctx context.Context) error {
|
||||
updates, err := th.Telegram.GetUpdates(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, update := range updates.Result {
|
||||
if update.Message.Chat.Type != "private" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(update.Message.Text, "/start ") {
|
||||
err := th.Telegram.Send(ctx, update.Message.Chat.ID, th.ErrorMsg)
|
||||
if err != nil {
|
||||
th.Logf("failed to notify telegram peer: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
token := strings.TrimPrefix(update.Message.Text, "/start ")
|
||||
|
||||
th.requests.RLock()
|
||||
authRequest, ok := th.requests.data[token]
|
||||
if !ok { // No such token
|
||||
th.requests.RUnlock()
|
||||
err := th.Telegram.Send(ctx, update.Message.Chat.ID, th.ErrorMsg)
|
||||
if err != nil {
|
||||
th.Logf("failed to notify telegram peer: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
th.requests.RUnlock()
|
||||
|
||||
avatarURL, err := th.Telegram.Avatar(ctx, update.Message.Chat.ID)
|
||||
if err != nil {
|
||||
th.Logf("failed to get user avatar: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
id := th.ProviderName + "_" + authtoken.HashID(sha1.New(), fmt.Sprint(update.Message.Chat.ID))
|
||||
|
||||
authRequest.confirmed = true
|
||||
authRequest.user = &authtoken.User{
|
||||
ID: id,
|
||||
Name: update.Message.Chat.Name,
|
||||
Picture: avatarURL,
|
||||
}
|
||||
|
||||
th.requests.Lock()
|
||||
th.requests.data[token] = authRequest
|
||||
th.requests.Unlock()
|
||||
|
||||
err = th.Telegram.Send(ctx, update.Message.Chat.ID, th.SuccessMsg)
|
||||
if err != nil {
|
||||
th.Logf("failed to notify telegram peer: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name of the provider
|
||||
func (th *TelegramHandler) Name() string { return th.ProviderName }
|
||||
|
||||
// Default token lifetime. Changed in tests
|
||||
var tgAuthRequestLifetime = time.Minute * 10
|
||||
|
||||
// LoginHandler generates and verifies login requests
|
||||
func (th *TelegramHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
queryToken := r.URL.Query().Get("token")
|
||||
if queryToken == "" {
|
||||
// GET /login (No token supplied)
|
||||
// Generate and send token
|
||||
token, err := randToken()
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, th.L, http.StatusInternalServerError, err, "failed to generate code")
|
||||
}
|
||||
|
||||
th.requests.Lock()
|
||||
th.requests.data[token] = tgAuthRequest{
|
||||
expires: time.Now().Add(tgAuthRequestLifetime),
|
||||
}
|
||||
th.requests.Unlock()
|
||||
|
||||
rest.RenderJSON(w, r, struct {
|
||||
Token string `json:"token"`
|
||||
Bot string `json:"bot"`
|
||||
}{token, th.username})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GET /login?token=blah
|
||||
th.requests.RLock()
|
||||
authRequest, ok := th.requests.data[queryToken]
|
||||
th.requests.RUnlock()
|
||||
|
||||
if !ok || time.Now().After(authRequest.expires) {
|
||||
th.requests.Lock()
|
||||
delete(th.requests.data, queryToken)
|
||||
th.requests.Unlock()
|
||||
|
||||
rest.SendErrorJSON(w, r, nil, http.StatusNotFound, nil, "request expired")
|
||||
return
|
||||
}
|
||||
|
||||
if !authRequest.confirmed {
|
||||
rest.SendErrorJSON(w, r, nil, http.StatusNotFound, nil, "request not yet confirmed")
|
||||
return
|
||||
}
|
||||
|
||||
u, err := setAvatar(th.AvatarSaver, *authRequest.user, &http.Client{Timeout: 5 * time.Second})
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, th.L, http.StatusInternalServerError, err, "failed to save avatar to proxy")
|
||||
return
|
||||
}
|
||||
|
||||
claims := authtoken.Claims{
|
||||
User: &u,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Id: queryToken,
|
||||
Issuer: th.ProviderName,
|
||||
},
|
||||
SessionOnly: false, // TODO
|
||||
}
|
||||
|
||||
if _, err := th.TokenService.Set(w, claims); err != nil {
|
||||
rest.SendErrorJSON(w, r, th.L, http.StatusInternalServerError, err, "failed to set token")
|
||||
return
|
||||
}
|
||||
|
||||
rest.RenderJSON(w, r, claims.User)
|
||||
|
||||
// Delete request
|
||||
th.requests.Lock()
|
||||
defer th.requests.Unlock()
|
||||
delete(th.requests.data, queryToken)
|
||||
}
|
||||
|
||||
// AuthHandler does nothing since we're don't have any callbacks
|
||||
func (th *TelegramHandler) AuthHandler(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
// LogoutHandler - GET /logout
|
||||
func (th *TelegramHandler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
th.TokenService.Reset(w)
|
||||
}
|
||||
|
||||
// tgAPI implements TelegramAPI
|
||||
type tgAPI struct {
|
||||
logger.L
|
||||
token string
|
||||
client *http.Client
|
||||
|
||||
// Identifier of the first update to be requested.
|
||||
// Should be equal to LastSeenUpdateID + 1
|
||||
// See https://core.telegram.org/bots/api#getupdates
|
||||
updateOffset int
|
||||
}
|
||||
|
||||
// NewTelegramAPI returns initialized TelegramAPI implementation
|
||||
func NewTelegramAPI(token string, client *http.Client) TelegramAPI {
|
||||
return &tgAPI{
|
||||
client: client,
|
||||
token: token,
|
||||
}
|
||||
}
|
||||
|
||||
// GetUpdates fetches incoming updates
|
||||
func (tg *tgAPI) GetUpdates(ctx context.Context) (*telegramUpdate, error) {
|
||||
url := `getUpdates?allowed_updates=["message"]`
|
||||
if tg.updateOffset != 0 {
|
||||
url += fmt.Sprintf("&offset=%d", tg.updateOffset)
|
||||
}
|
||||
|
||||
var result telegramUpdate
|
||||
|
||||
err := tg.request(ctx, url, &result)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to fetch updates")
|
||||
}
|
||||
|
||||
for _, u := range result.Result {
|
||||
if u.UpdateID >= tg.updateOffset {
|
||||
tg.updateOffset = u.UpdateID + 1
|
||||
}
|
||||
}
|
||||
|
||||
return &result, err
|
||||
}
|
||||
|
||||
// Send sends a message to telegram peer
|
||||
func (tg *tgAPI) Send(ctx context.Context, id int, msg string) error {
|
||||
url := fmt.Sprintf("sendMessage?chat_id=%d&text=%s", id, neturl.PathEscape(msg))
|
||||
return tg.request(ctx, url, &struct{}{})
|
||||
}
|
||||
|
||||
// Avatar returns URL to user avatar
|
||||
func (tg *tgAPI) Avatar(ctx context.Context, id int) (string, error) {
|
||||
// Get profile pictures
|
||||
url := fmt.Sprintf(`getUserProfilePhotos?user_id=%d`, id)
|
||||
|
||||
var profilePhotos = struct {
|
||||
Result struct {
|
||||
Photos [][]struct {
|
||||
ID string `json:"file_id"`
|
||||
} `json:"photos"`
|
||||
} `json:"result"`
|
||||
}{}
|
||||
|
||||
if err := tg.request(ctx, url, &profilePhotos); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// User does not have profile picture set or it is hidden in privacy settings
|
||||
if len(profilePhotos.Result.Photos) == 0 || len(profilePhotos.Result.Photos[0]) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Get max possible picture size
|
||||
last := len(profilePhotos.Result.Photos[0]) - 1
|
||||
fileID := profilePhotos.Result.Photos[0][last].ID
|
||||
url = fmt.Sprintf(`getFile?file_id=%s`, fileID)
|
||||
|
||||
var fileMetadata = struct {
|
||||
Result struct {
|
||||
Path string `json:"file_path"`
|
||||
} `json:"result"`
|
||||
}{}
|
||||
|
||||
if err := tg.request(ctx, url, &fileMetadata); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
avatarURL := fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", tg.token, fileMetadata.Result.Path)
|
||||
|
||||
return avatarURL, nil
|
||||
}
|
||||
|
||||
type botInfo struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"first_name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// BotInfo returns info about configured bot
|
||||
func (tg *tgAPI) BotInfo(ctx context.Context) (*botInfo, error) {
|
||||
var resp = struct {
|
||||
Result *botInfo `json:"result"`
|
||||
}{}
|
||||
|
||||
err := tg.request(ctx, "getMe", &resp)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to fetch bot info")
|
||||
}
|
||||
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
func (tg *tgAPI) request(ctx context.Context, method string, data interface{}) error {
|
||||
repeat := repeater.NewDefault(3, time.Millisecond*50)
|
||||
|
||||
return repeat.Do(ctx, func() error {
|
||||
url := fmt.Sprintf("https://api.telegram.org/bot%s/%s", tg.token, method)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create request")
|
||||
}
|
||||
|
||||
resp, err := tg.client.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to send request")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return tg.parseError(resp.Body)
|
||||
}
|
||||
|
||||
if err = json.NewDecoder(resp.Body).Decode(data); err != nil {
|
||||
return errors.Wrap(err, "failed to decode json response")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (tg *tgAPI) parseError(r io.Reader) error {
|
||||
var tgErr = struct {
|
||||
Description string `json:"description"`
|
||||
}{}
|
||||
|
||||
if err := json.NewDecoder(r).Decode(&tgErr); err != nil {
|
||||
return errors.Wrap(err, "can't decode error")
|
||||
}
|
||||
|
||||
return errors.Errorf("telegram returned error: %v", tgErr.Description)
|
||||
}
|
||||
+6
@@ -66,6 +66,7 @@ type Opts struct {
|
||||
AudienceReader Audience // allowed aud values
|
||||
Issuer string // optional value for iss claim, usually application name
|
||||
AudSecrets bool // uses different secret for differed auds. important: adds pre-parsing of unverified token
|
||||
SendJWTHeader bool // if enabled send JWT as a header instead of cookie
|
||||
}
|
||||
|
||||
// NewService makes JWT service
|
||||
@@ -226,6 +227,11 @@ func (j *Service) Set(w http.ResponseWriter, claims Claims) (Claims, error) {
|
||||
return Claims{}, errors.Wrap(err, "failed to make token token")
|
||||
}
|
||||
|
||||
if j.SendJWTHeader {
|
||||
w.Header().Set(j.JWTHeaderKey, tokenString)
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
cookieExpiration := 0 // session cookie
|
||||
if !claims.SessionOnly && claims.Handshake == nil {
|
||||
cookieExpiration = int(j.CookieDuration.Seconds())
|
||||
|
||||
+11
@@ -30,6 +30,7 @@ type User struct {
|
||||
IP string `json:"ip,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Attributes map[string]interface{} `json:"attrs,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
}
|
||||
|
||||
// SetBoolAttr sets boolean attribute
|
||||
@@ -145,3 +146,13 @@ func SetUserInfo(r *http.Request, user User) *http.Request {
|
||||
ctx = context.WithValue(ctx, contextKey("user"), user)
|
||||
return r.WithContext(ctx)
|
||||
}
|
||||
|
||||
// SetRole sets user role for RBAC
|
||||
func (u *User) SetRole(role string) {
|
||||
u.Role = role
|
||||
}
|
||||
|
||||
// GetRole gets user role
|
||||
func (u *User) GetRole() string {
|
||||
return u.Role
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -72,7 +72,7 @@ github.com/go-chi/cors
|
||||
# github.com/go-chi/render v1.0.1
|
||||
## explicit
|
||||
github.com/go-chi/render
|
||||
# github.com/go-pkgz/auth v0.11.0
|
||||
# github.com/go-pkgz/auth v1.13.0
|
||||
## explicit
|
||||
github.com/go-pkgz/auth
|
||||
github.com/go-pkgz/auth/avatar
|
||||
|
||||
Reference in New Issue
Block a user