-
Notifications
You must be signed in to change notification settings - Fork 2
/
module_entrypoint.go
174 lines (148 loc) · 4.66 KB
/
module_entrypoint.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
package caddydiscord
import (
"encoding/hex"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/caddyauth"
"golang.org/x/oauth2"
"net/http"
"time"
)
var (
_ caddyfile.Unmarshaler = (*ProtectorPlugin)(nil)
_ caddy.Validator = (*ProtectorPlugin)(nil)
_ caddyauth.Authenticator = (*ProtectorPlugin)(nil)
)
func init() {
caddy.RegisterModule(ProtectorPlugin{})
httpcaddyfile.RegisterHandlerDirective("protect", parseCaddyfileHandlerDirective2)
}
func parseCaddyfileHandlerDirective2(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var s ProtectorPlugin
s.UnmarshalCaddyfile(h.Dispenser)
return caddyauth.Authentication{
ProvidersRaw: caddy.ModuleMap{
"discord": caddyconfig.JSON(s, nil),
},
}, nil
}
// ProtectorPlugin allows you to authenticate caddy routes from
// a Discord User Identity.
//
// e.g. Accessing /really-cool-people requires user to have {Role}
// within {Guild}
//
// Discord's OAuth flow is used for identity using your
// own Discord developer application.
//
// See an example Caddyfile https://github.com/enum-gg/caddy-discord#caddyfile-example
type ProtectorPlugin struct {
OAuthConfig *oauth2.Config
tokenSigner TokenSignerSignature
authedTokenParser AuthedTokenParserSignature
flowTokenParser FlowTokenParserSignature
Realm string
cookie CookieNamer
}
// Authenticate implements caddyhttp.MiddlewareHandler.
func (p *ProtectorPlugin) Authenticate(w http.ResponseWriter, r *http.Request) (caddyauth.User, bool, error) {
existingSession, _ := r.Cookie(p.cookie(p.Realm))
// Handle passing through signed token over to support multiple domains.
// TODO: Refactor this code into oblivion.
if existingSession == nil && r.URL.Query().Has("DISCO_PASSTHROUGH") && r.URL.Query().Has("DISCO_REALM") {
q := r.URL.Query()
signedToken := q.Get("DISCO_PASSTHROUGH")
realm := q.Get("DISCO_REALM")
q.Del("DISCO_PASSTHROUGH")
q.Del("DISCO_REALM")
r.URL.RawQuery = q.Encode()
// TODO: Expires should be reduced if authorisation failed.
cookie := &http.Cookie{
Name: p.cookie(realm),
Value: signedToken,
Expires: time.Now().Add(time.Hour * 16),
HttpOnly: true,
// Strict mode breaks functionality - due to discord referrer.
SameSite: http.SameSiteLaxMode,
Path: "/",
//Secure // TODO: Configurable
}
http.SetCookie(w, cookie)
http.Redirect(w, r, r.URL.String(), http.StatusFound)
return caddyauth.User{}, false, nil
}
if existingSession != nil {
claims, err := p.authedTokenParser(existingSession.Value)
if err != nil {
return caddyauth.User{}, false, err
}
return caddyauth.User{
ID: claims.Subject,
Metadata: map[string]string{
"username": claims.Username,
"avatar": claims.Avatar,
},
}, claims.Authorised, nil
}
// 15 minutes to make it through Discord consent.
exp := time.Now().Add(time.Minute * 15)
backToURL := *r.URL
if !backToURL.IsAbs() {
backToURL.Scheme = "http"
if r.TLS != nil {
backToURL.Scheme = "https"
}
backToURL.Host = r.Host
}
token := NewAuthFlowToken(backToURL.String(), p.Realm, exp)
signedToken, err := p.tokenSigner(token)
if err != nil {
// Unable to generate JWT
http.Error(w, "Failed to generate token", http.StatusInternalServerError)
return caddyauth.User{}, false, err
}
url := p.OAuthConfig.AuthCodeURL(signedToken, oauth2.SetAuthURLParam("prompt", "none"))
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
return caddyauth.User{}, false, nil
}
func (p *ProtectorPlugin) Provision(ctx caddy.Context) error {
ctxApp, _ := ctx.App(moduleName)
app := ctxApp.(*DiscordPortalApp)
p.cookie = CookieName(app.ExecutionKey)
p.OAuthConfig = app.getOAuthConfig()
key, err := hex.DecodeString(app.Key)
if err != nil {
return err
}
p.tokenSigner = NewTokenSigner(key)
p.authedTokenParser = NewAuthedTokenParser(key)
p.flowTokenParser = NewFlowTokenParser(key)
return nil
}
func (ProtectorPlugin) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.authentication.providers.discord",
New: func() caddy.Module { return new(ProtectorPlugin) },
}
}
func (p *ProtectorPlugin) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
if d.NextArg() {
// allow "with" or "using"
if d.Val() != "with" && d.Val() != "using" {
return d.ArgErr()
}
if !d.NextArg() {
return d.ArgErr()
}
p.Realm = d.Val()
}
}
return nil
}
func (p *ProtectorPlugin) Validate() error {
return nil
}