-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Zitadel authorization has been implemented. A new middleware uses Zitadel's API to introspect a given token and verify it is valid. This prevents unauthorized users from accesing the API. Closes #17.
- Loading branch information
1 parent
2cf6f93
commit 987a3f8
Showing
3 changed files
with
147 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,6 @@ | ||
# eryth | ||
API to perform configuration management on Asterisk servers. Project named after | ||
[Erythrina americana](https://mexico.inaturalist.org/taxa/201455-Erythrina-americana). | ||
|
||
## Configuration | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
package zitadel | ||
|
||
import ( | ||
"encoding/base64" | ||
"encoding/json" | ||
"errors" | ||
"log/slog" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
"time" | ||
) | ||
|
||
type Options struct { | ||
Host string | ||
ProjectID string | ||
ClientID string | ||
ClientSecret string | ||
} | ||
|
||
type tokenInfo struct { | ||
Active bool `json:"active"` | ||
Audience []string `json:"aud,omitempty"` | ||
ClientID string `json:"client_id,omitempty"` | ||
Expires int64 `json:"exp,omitempty"` | ||
Issuer string `json:"iss,omitempty"` | ||
IssuedAt int64 `json:"iat,omitempty"` | ||
ID string `json:"jti,omitempty"` | ||
NotBefore int64 `json:"nbf,omitempty"` | ||
Scope string `json:"scope,omitempty"` | ||
Type string `json:"token_type,omitempty"` | ||
Username string `json:"username,omitempty"` | ||
} | ||
|
||
type zitadel struct { | ||
opts Options | ||
client *http.Client | ||
} | ||
|
||
func (z *zitadel) handler(next http.Handler) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") | ||
if token == "" { | ||
w.WriteHeader(http.StatusUnauthorized) | ||
return | ||
} | ||
info, err := z.introspectToken(token) | ||
if err != nil { | ||
slog.Error("Failed to introspect ZITADEL token", slog.String("reason", err.Error())) | ||
w.WriteHeader(http.StatusUnauthorized) | ||
return | ||
} | ||
|
||
err = z.validateToken(info) | ||
if err != nil { | ||
slog.Info("Rejected token", slog.String("reason", err.Error())) | ||
w.WriteHeader(http.StatusUnauthorized) | ||
return | ||
} | ||
|
||
next.ServeHTTP(w, r) | ||
}) | ||
} | ||
|
||
func (z *zitadel) validateToken(info tokenInfo) error { | ||
if !info.Active { | ||
return errors.New("token is inactive") | ||
} | ||
|
||
properAudience := false | ||
for _, aud := range info.Audience { | ||
if aud == z.opts.ProjectID { | ||
properAudience = true | ||
break | ||
} | ||
} | ||
if !properAudience { | ||
return errors.New("token is missing proper audience") | ||
} | ||
|
||
if z.opts.Host != info.Issuer { | ||
return errors.New("token issuer does not match zitadel host") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (z *zitadel) introspectToken(token string) (tokenInfo, error) { | ||
form := url.Values{} | ||
form.Add("token", token) | ||
|
||
path, err := url.JoinPath(z.opts.Host, "/oauth/v2/introspect") | ||
if err != nil { | ||
return tokenInfo{}, err | ||
} | ||
|
||
req, err := http.NewRequest("POST", path, strings.NewReader(form.Encode())) | ||
if err != nil { | ||
return tokenInfo{}, err | ||
} | ||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") | ||
req.Header.Set( | ||
"Authorization", | ||
"Basic "+base64.StdEncoding.EncodeToString([]byte(z.opts.ClientID+":"+z.opts.ClientSecret)), | ||
) | ||
|
||
res, err := z.client.Do(req) | ||
if err != nil { | ||
return tokenInfo{}, err | ||
} | ||
defer res.Body.Close() | ||
|
||
var info tokenInfo | ||
decoder := json.NewDecoder(res.Body) | ||
err = decoder.Decode(&info) | ||
if err != nil { | ||
return tokenInfo{}, err | ||
} | ||
|
||
return info, nil | ||
} | ||
|
||
func Middleware(opts Options) func(http.Handler) http.Handler { | ||
z := &zitadel{ | ||
opts: opts, | ||
client: &http.Client{Timeout: 10 * time.Second}, | ||
} | ||
|
||
return z.handler | ||
} |