-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: add implementation examples (#3)
Add example of entire DPoP authorization flow, from creating a bound token to accepting that bound token. The example can be run with a simple bash script that starts local servers and runs a client that attempts to get a resource.
- Loading branch information
1 parent
43d01ee
commit 570b226
Showing
9 changed files
with
492 additions
and
7 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
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 |
---|---|---|
|
@@ -5,7 +5,7 @@ on: | |
branches: | ||
- main | ||
paths: | ||
- "**.go" | ||
- "*.go" | ||
|
||
jobs: | ||
vet-and-test: | ||
|
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 |
---|---|---|
|
@@ -21,4 +21,7 @@ | |
go.work | ||
|
||
# Visual Studio Code workspace settings | ||
.vscode/ | ||
.vscode/ | ||
|
||
# Example binaries | ||
build/ |
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,17 @@ | ||
# Example Implementation | ||
|
||
Working implementation of authorization server, resource server and a client that communicates with them. | ||
|
||
Run: | ||
|
||
```sh | ||
./run-example.sh | ||
``` | ||
|
||
This will start 2 servers running on localhost, an authorization server and a resource server. | ||
When the enter key is pressed (this allows the servers to start correctly) the client will be started and attempt to get the resource from the resource server. | ||
|
||
The client will create a private `ES256` key and create a proof that will be used to request a bound token from the authorization server. | ||
The returned bound token will then be sent together with a new bound proof to the resource server, the resource server will then validate the signature of the bound token, the signature of the proof and the binding between them. | ||
|
||
If everything is validated successfully the resource server will respond with the resource to the client. |
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,162 @@ | ||
package main | ||
|
||
import ( | ||
"crypto/ecdsa" | ||
"crypto/elliptic" | ||
"crypto/rand" | ||
"encoding/base64" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"time" | ||
|
||
"github.com/AxisCommunications/go-dpop" | ||
"github.com/golang-jwt/jwt/v5" | ||
) | ||
|
||
var privateKey *ecdsa.PrivateKey | ||
|
||
type BoundTokenClaims struct { | ||
*dpop.BoundAccessTokenClaims | ||
Scope []string `json:"scope"` | ||
} | ||
|
||
type ECJWK struct { | ||
KTY string `json:"kty"` | ||
CRV string `json:"crv"` | ||
KID string `json:"kid"` | ||
X string `json:"x"` | ||
Y string `json:"y"` | ||
} | ||
|
||
// postToken will accept requests to create a bound a token | ||
func postToken(w http.ResponseWriter, r *http.Request) { | ||
fmt.Printf("Authorization server - got /token request\n") | ||
httpUrl, _ := url.Parse("https://server.example.com/token") | ||
|
||
// read proof header | ||
proofString := r.Header.Get("dpop") | ||
if proofString == "" { | ||
fmt.Println("No proof") | ||
w.WriteHeader(http.StatusBadRequest) | ||
return | ||
} | ||
|
||
// validate proof | ||
acceptedTimeWindow := time.Hour * 24 * 365 * 10 | ||
proof, err := dpop.Parse(proofString, dpop.POST, httpUrl, dpop.ParseOptions{ | ||
TimeWindow: &acceptedTimeWindow, | ||
}) | ||
// Check the error type to determine response | ||
if err != nil { | ||
if ok := errors.Is(err, dpop.ErrInvalidProof); ok { | ||
w.WriteHeader(http.StatusBadRequest) | ||
io.WriteString(w, err.Error()) | ||
return | ||
} | ||
} | ||
|
||
// proof is valid, get public key to associate with access token | ||
jkt := proof.PublicKey() | ||
|
||
// The server should check credentials of calling user here to ensure that the user has access to this api | ||
// but is skipped here for simplicity. | ||
|
||
// read body | ||
defer r.Body.Close() | ||
b, err := io.ReadAll(r.Body) | ||
if err != nil { | ||
fmt.Println(err) | ||
w.WriteHeader(http.StatusBadRequest) | ||
io.WriteString(w, "unreadable body") | ||
return | ||
} | ||
|
||
// parse body | ||
data := &struct { | ||
Resource string `json:"resource"` | ||
Scope []string `json:"scope"` | ||
}{} | ||
err = json.Unmarshal(b, &data) | ||
if err != nil { | ||
fmt.Println(err) | ||
w.WriteHeader(http.StatusBadRequest) | ||
io.WriteString(w, "malformed body") | ||
return | ||
} | ||
|
||
// It is a good idea to validate that the caller has the access that they are requesting | ||
// but is skipped here for simplicity. | ||
|
||
// create bound JWT | ||
claims := &BoundTokenClaims{ | ||
BoundAccessTokenClaims: &dpop.BoundAccessTokenClaims{ | ||
RegisteredClaims: &jwt.RegisteredClaims{ | ||
Issuer: "example.com", | ||
Subject: "user", | ||
Audience: []string{data.Resource}, | ||
IssuedAt: jwt.NewNumericDate(time.Now()), | ||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), | ||
ID: "unique-id", | ||
}, | ||
Confirmation: dpop.Confirmation{JWKThumbprint: jkt}, | ||
}, | ||
Scope: data.Scope, | ||
} | ||
boundToken := jwt.NewWithClaims(jwt.SigningMethodES256, claims) | ||
boundToken.Header["kid"] = "auth-key" | ||
boundTokenString, err := boundToken.SignedString(privateKey) | ||
if err != nil { | ||
fmt.Println(err) | ||
w.WriteHeader(http.StatusInternalServerError) | ||
io.WriteString(w, "could not create token") | ||
return | ||
} | ||
|
||
// return signed bound JWT | ||
io.WriteString(w, boundTokenString) | ||
} | ||
|
||
// getKeys returns a list of public keys that can be used to verify bound tokens. | ||
func getKeys(w http.ResponseWriter, r *http.Request) { | ||
fmt.Printf("Authorization server - got /keys request\n") | ||
jwks := &struct { | ||
Keys []ECJWK `json:"keys"` | ||
}{[]ECJWK{{ | ||
KTY: "EC", | ||
CRV: privateKey.PublicKey.Params().Name, | ||
KID: "auth-key", | ||
X: base64.RawURLEncoding.Strict().EncodeToString(privateKey.PublicKey.X.Bytes()), | ||
Y: base64.RawURLEncoding.Strict().EncodeToString(privateKey.PublicKey.Y.Bytes()), | ||
}}} | ||
|
||
response, err := json.Marshal(jwks) | ||
if err != nil { | ||
fmt.Println(err) | ||
w.WriteHeader(http.StatusInternalServerError) | ||
io.WriteString(w, err.Error()) | ||
return | ||
} | ||
|
||
io.WriteString(w, string(response)) | ||
} | ||
|
||
func main() { | ||
// generate private key that will be used to sign authorization server tokens | ||
var err error | ||
privateKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | ||
if err != nil { | ||
fmt.Println(err) | ||
return | ||
} | ||
|
||
// start server | ||
http.HandleFunc("/token", postToken) | ||
http.HandleFunc("/keys", getKeys) | ||
|
||
err = http.ListenAndServe(":1337", nil) | ||
fmt.Println(err) | ||
} |
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,124 @@ | ||
package main | ||
|
||
import ( | ||
"bytes" | ||
"crypto/ecdsa" | ||
"crypto/elliptic" | ||
"crypto/rand" | ||
"crypto/sha256" | ||
"encoding/base64" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/AxisCommunications/go-dpop" | ||
"github.com/golang-jwt/jwt/v5" | ||
) | ||
|
||
type tokenRequestBody struct { | ||
Resource string `json:"resource"` | ||
Scope []string `json:"scope"` | ||
} | ||
|
||
func main() { | ||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
// Create a DPoP proof token in order to request a bound token from the autorization server | ||
claims := dpop.ProofTokenClaims{ | ||
RegisteredClaims: &jwt.RegisteredClaims{ | ||
Issuer: "client", | ||
Subject: "user", | ||
Audience: jwt.ClaimStrings{"https://server.example.com/token"}, | ||
ID: "random_id", | ||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute)), | ||
IssuedAt: jwt.NewNumericDate(time.Now()), | ||
}, | ||
Method: dpop.POST, | ||
URL: "https://server.example.com/token", | ||
} | ||
proof, err := dpop.Create(jwt.SigningMethodES256, &claims, privateKey) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
// Request a bound token from the autorization server | ||
fmt.Println("Client - requesting a bound token from the authorization server") | ||
body := tokenRequestBody{ | ||
Resource: "https://server.example.com/resource", | ||
Scope: []string{"read", "write"}, | ||
} | ||
jsonBody, err := json.Marshal(body) | ||
if err != nil { | ||
panic(err) | ||
} | ||
req, err := http.NewRequest("POST", "http://localhost:1337/token", bytes.NewReader(jsonBody)) | ||
if err != nil { | ||
panic(err) | ||
} | ||
req.Header.Add("dpop", proof) | ||
res, err := http.DefaultClient.Do(req) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
// Read the bound token from the response | ||
boundTokenString, err := io.ReadAll(res.Body) | ||
if err != nil { | ||
panic(err) | ||
} | ||
res.Body.Close() | ||
fmt.Printf("Client - received bound token: %s\n", boundTokenString) | ||
|
||
// Create a bound DPoP proof token in order to access the resource server | ||
h := sha256.New() | ||
_, err = h.Write([]byte(boundTokenString)) | ||
if err != nil { | ||
panic(err) | ||
} | ||
ath := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) | ||
claims = dpop.ProofTokenClaims{ | ||
RegisteredClaims: &jwt.RegisteredClaims{ | ||
Issuer: "client", | ||
Subject: "user", | ||
Audience: jwt.ClaimStrings{"https://server.example.com/resource"}, | ||
ID: "random_id", | ||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute)), | ||
IssuedAt: jwt.NewNumericDate(time.Now()), | ||
}, | ||
Method: dpop.GET, | ||
URL: "https://server.example.com/resource", | ||
// This binds the proof to the bound token | ||
AccessTokenHash: ath, | ||
} | ||
// Sign with the same private key used to sign the proof that was sent to the authorization server | ||
boundProof, err := dpop.Create(jwt.SigningMethodES256, &claims, privateKey) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
// Access the resource server | ||
fmt.Println("Client - accessing the resource server") | ||
req, err = http.NewRequest("POST", "http://localhost:40000/resource", bytes.NewReader(jsonBody)) | ||
if err != nil { | ||
panic(err) | ||
} | ||
req.Header.Add("dpop", boundProof) | ||
req.Header.Add("Authorization", fmt.Sprintf("dpop %s", boundTokenString)) | ||
resourceRes, err := http.DefaultClient.Do(req) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
// Read the resource server response | ||
defer res.Body.Close() | ||
resourceBody, err := io.ReadAll(resourceRes.Body) | ||
if err != nil { | ||
panic(err) | ||
} | ||
fmt.Printf("Client - resource server response: %s\n", string(resourceBody)) | ||
} |
Oops, something went wrong.