Skip to content

Commit

Permalink
Implementation of authorization code grant (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
oxisto authored Feb 28, 2022
1 parent 913b082 commit 482fc19
Show file tree
Hide file tree
Showing 12 changed files with 897 additions and 98 deletions.
7 changes: 5 additions & 2 deletions auth.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package oauth2

type Client struct {
clientID string
clientSecret string
ClientID string

ClientSecret string

RedirectURI string
}
12 changes: 9 additions & 3 deletions cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@ var ctx func(net.Listener) context.Context = nil
func main() {
flag.Parse()

password := login.GeneratePassword()
userPassword := oauth2.GenerateSecret()
clientPassword := oauth2.GenerateSecret()

log.Printf("Creating new user admin with password %s", password)
log.Printf(`Creating new user "admin" with password %s`, userPassword)
log.Printf(`Creating new client "client" with password %s`, clientPassword)

srv = oauth2.NewServer(fmt.Sprintf(":%d", *port), login.WithLoginPage(login.WithUser("admin", password)))
srv = oauth2.NewServer(
fmt.Sprintf(":%d", *port),
oauth2.WithClient("client", clientPassword, ""),
login.WithLoginPage(login.WithUser("admin", userPassword)),
)
srv.BaseContext = ctx

log.Printf("Creating new OAuth 2.0 server on :%d", *port)
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ go 1.16
require (
github.com/golang-jwt/jwt/v4 v4.3.0
golang.org/x/crypto v0.0.0-20220214200702-86341886e292
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,18 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down Expand Up @@ -226,13 +231,16 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
Expand Down
124 changes: 121 additions & 3 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,35 @@ import (
"fmt"
"log"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"testing"

"github.com/golang-jwt/jwt/v4"
oauth2 "github.com/oxisto/oauth2go"
"github.com/oxisto/oauth2go/login"
"golang.org/x/net/html"
"golang.org/x/oauth2/clientcredentials"
)

func TestIntegration(t *testing.T) {
srv := oauth2.NewServer(":0", oauth2.WithClient("client", "secret"))
srv := oauth2.NewServer(":0", oauth2.WithClient("client", "secret", ""))
ln, err := net.Listen("tcp", srv.Addr)
if err != nil {
t.Errorf("Error while listening key: %v", err)
}

port := ln.Addr().(*net.TCPAddr).Port

go srv.Serve(ln)
defer srv.Close()

config := clientcredentials.Config{
ClientID: "client",
ClientSecret: "secret",
TokenURL: fmt.Sprintf("http://localhost:%d/token", ln.Addr().(*net.TCPAddr).Port),
TokenURL: fmt.Sprintf("http://localhost:%d/token", port),
}

token, err := config.Token(context.Background())
Expand All @@ -36,11 +44,121 @@ func TestIntegration(t *testing.T) {
log.Printf("Token: %s", token.AccessToken)

jwtoken, err := jwt.ParseWithClaims(token.AccessToken, &jwt.RegisteredClaims{}, func(t *jwt.Token) (interface{}, error) {
return srv.PublicKey(), nil
kid, _ := strconv.ParseInt(t.Header["kid"].(string), 10, 64)

return srv.PublicKeys()[kid], nil
})
if err != nil {
t.Errorf("Error while retrieving a token: %v", err)
}

log.Printf("JWT: %+v", jwtoken)
}

func TestThreeLeggedFlow(t *testing.T) {
var (
res *http.Response
req *http.Request
client *http.Client
form url.Values
session *http.Cookie
token *oauth2.Token
code string
)

srv := oauth2.NewServer(":0",
oauth2.WithClient("client", "secret", "/test"),
login.WithLoginPage(login.WithUser("admin", "admin")),
)

ln, err := net.Listen("tcp", srv.Addr)
if err != nil {
t.Errorf("Error while listening key: %v", err)
}

port := ln.Addr().(*net.TCPAddr).Port

go srv.Serve(ln)
defer srv.Close()

config := oauth2.Config{
ClientID: "client",
ClientSecret: "secret",
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("http://localhost:%d/authorize", port),
TokenURL: fmt.Sprintf("http://localhost:%d/token", port),
},
RedirectURL: "/test",
}

// Let's pretend to be a browser
res, _ = http.Get(config.AuthCodeURL("some-state"))

// We are interested in two things
// - The session ID (or the cookie)
// - The CSRF token
for _, c := range res.Cookies() {
if c.Name == "id" {
session = c
break
}
}

// Parse the HTML body to look for the csrf_token
root, _ := html.Parse(res.Body)

form = url.Values{}
walker := func(node *html.Node) {
if node.Type == html.ElementNode &&
node.Data == "input" &&
len(node.Attr) == 3 {
form.Add(node.Attr[1].Val, node.Attr[2].Val)
}
}

traverse(root, walker)

form.Add("username", "admin")
form.Add("password", "admin")

req, _ = http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/login", port), strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(session)

// Let's POST our login
client = &http.Client{}
res, err = client.Do(req)
if err != nil {
t.Errorf("Error while POST /login: %v", err)
}

// Extract the code from the response
code = res.Request.URL.Query().Get("code")

token, err = config.Exchange(context.Background(), code)
if err != nil {
t.Errorf("Error while Exchange: %v", err)
}

if token.AccessToken == "" {
t.Error("Access token is empty", err)
}

if token.RefreshToken == "" {
t.Error("Access token is empty", err)
}
}

func traverse(root *html.Node, walker func(node *html.Node)) {
var f func(*html.Node)

f = func(n *html.Node) {
walker(n)

for c := n.FirstChild; c != nil; c = c.NextSibling {
f(c)
}
}

f(root)
}
59 changes: 59 additions & 0 deletions login/authorize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package login

import (
"fmt"
"net/http"
"net/url"

oauth2 "github.com/oxisto/oauth2go"
)

// handleAuthorize implements the authorize endpoint (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1).
func (h *handler) handleAuthorize(w http.ResponseWriter, r *http.Request) {
var (
client *oauth2.Client
redirectURI string
state string
err error
query url.Values
session *session
)

query = r.URL.Query()

client, err = h.srv.GetClient(query.Get("client_id"))
if err != nil {
http.Error(w, "Invalid client ID", http.StatusBadRequest)
return
}

redirectURI = query.Get("redirect_uri")
if redirectURI == "" || client.RedirectURI != redirectURI {
http.Error(w, "Invalid redirect URI", http.StatusBadRequest)
return
}

if query.Get("response_type") != "code" {
oauth2.RedirectError(w, r, redirectURI, "invalid_request")
return
}

state = query.Get("state")

// Check, if we already have a session
session = h.extractSession(w, r)

if session.Anonymous() {
var params = url.Values{}
params.Add("return_url", r.RequestURI)

// Redirect to our login page
http.Redirect(w, r, fmt.Sprintf("/login?%s", params.Encode()), http.StatusFound)
} else {
var params = url.Values{}
params.Add("code", h.srv.IssueCode())
params.Add("state", state)

http.Redirect(w, r, fmt.Sprintf("%s?%s", redirectURI, params.Encode()), http.StatusFound)
}
}
Loading

0 comments on commit 482fc19

Please sign in to comment.