add the request authentication

Gerasimos (Makis) Maropoulos 2019-07-04 21:44:35 +03:00
parent 7a7c2fded2
commit f1ef874ee1
No known key found for this signature in database
GPG Key ID: F169457BBDA4ACF4
4 changed files with 386 additions and 3 deletions

@ -19,6 +19,7 @@ This wiki is the main source of documentation for **developers** working with (o
* [[Wrap the Router|Routing-wrap-the-router]] * [[Wrap the Router|Routing-wrap-the-router]]
* [[Override Context|Routing-override-context]] * [[Override Context|Routing-override-context]]
* [[API Versioning]] * [[API Versioning]]
* [[Request Authentication]]
* [[File Server]] * [[File Server]]
* [[View]] * [[View]]
* [[Dependency Injection|dependency-injection]] * [[Dependency Injection|dependency-injection]]

381
Request-authentication.md Normal file

@ -0,0 +1,381 @@
Iris offers secure and modern request authentication for your web applications. Signing and verifying data featured inside the iris/crypto subpackage. Like the cryptocurrencies, Iris uses the Elliptic Curve Digital Signature Algorithm to sign and verify data.
The Iris crypto package offers authentication (with optional encryption in top of) and verification of raw `[]byte` data with `crypto.Marshal/Unmarshal` package-level functions.
Higher level implementation is also available. The `crypto.SignJSON/VerifyJSON` package-level functions accept JSON payloads.
> You can skip the above section and take ahead to the example code if you already know what you're doing.
## ECDSA Private - Public Key pair
Let's begin by learning how you can generate or parse an existing ECDSA private and public key pair which is required for signing and verification. The private key is used for signing, the public key is used for verification.
### Generate
The `MustGenerateKey` package-level function generates an ECDSA public and private key pair. It panics if any error occurred.
```go
MustGenerateKey() *ecdsa.PrivateKey
```
### Parse
However, on a production level application you may have already stored a private key somewhere in the local system or to a database per client.
There are two functions, when you have access to the private key use the `ParsePrivateKey`. Remember, the `*ecdsa.PrivateKey` can provide you its public key through its `.PublicKey` field. Otherwise if you just want the public key use the `ParsePublicKey` instead.
```go
ParsePrivateKey(key []byte) (*ecdsa.PrivateKey, error)
ParsePublicKey(key []byte) (*ecdsa.PublicKey, error)
```
Both of these functions accept any `key []byte`, it may be retrieved from a database or from a local file reader, the implementation doesn't really care from where you access this.
## Sign and Verify JSON
Let's continue by taking look at the `SignJSON` and `VerifyJSON` as this is the most common scenario for a web application.
The `SignJSON` package-level function signs the incoming JSON request payload based on
a "privateKey" and the "r" (could be ctx.Request().Body).
It generates the signature and returns a `Ticket` structure value.
The `Ticket` contains the original client's payload raw data
and the generated signature.
```go
SignJSON(privateKey *ecdsa.PrivateKey, r io.Reader) (Ticket, error)
```
**Usage**
```go
ticket, err := crypto.SignJSON(testPrivateKey, ctx.Request().Body)
// ticket.Signature is the ECDSA signature.
// ticket.Payload is the original payload readen from the request body.
```
The `VerifyJSON` package-level function verifies the incoming JSON request,
by reading the "r" which should decodes to a `Ticket`.
The `Ticket` is verified against the given "publicKey", the `Ticket.Signature` and
`Ticket.Payload` data (original request's payload data which was signed by `SignJSON`).
Returns true wether the verification succeed or not.
The "ptr" input argument should be a pointer to a value of the same payload structure the client signed on.
If and only if the verification succeed the payload value is filled from the `Ticket.Payload` raw data.
Check for both output arguments in order to:
1. verification (true/false and error output arguments) and
2. ticket's original json payload parsed and "ptr" is filled successfully (error output argument).
```go
VerifyJSON(publicKey *ecdsa.PublicKey, r io.Reader, ptr interface{}) (bool, error)
```
**Usage**
```go
var myPayload myJSONStruct
ok, err := crypto.VerifyJSON(publicKey, ctx.Request().Body, &myPayload)
```
## Sign and Verify a slice of `[]byte`
The lower-level implementation lives inside the `crypto.Marshal` and `crypto.Unmarshal` package-level functions. Both of them accept a `crypto.Encryption` or `crypto.Decryption` to encode and decode more securely but they are totally optional.
The `Marshal` function signs and, optionally, encrypts the "data".
The form of the output value is: signature_of_88_length followed by the raw_data_or_encrypted_data,
i.e "R+eqxA3LslRif0KoxpevpNILAs4Kh4mccCCoE0sRjICkj9xy0/gsxeUd2wfcGK5mzIZ6tM3A939Wjif0xwZCog==7001f30..."
Returns non-nil error if any error occurred.
```go
Marshal(privateKey *ecdsa.PrivateKey, data []byte, enc Encryption) ([]byte, error)
```
**Usage**
```go
data, _ := ioutil.ReadAll(ctx.Request().Body)
signedData, err := crypto.Marshal(testPrivateKey, data, nil)
w.Write(signedData)
```
Or if data should be **encrypted**:
```go
signedEncryptedData, err := crypto.Marshal(
testPrivateKey,
data,
crypto.Encrypt(aesKey, nil),
)
```
The `Unmarshal` function verifies the "data" and, optionally, decrypts the output.
Returns returns the signed raw data; without the signature and decrypted if "decrypt" is not nil.
The second output value reports whether the verification and any decryption of the data succeed or not.
```go
Unmarshal(publicKey *ecdsa.PublicKey, data []byte, dec Decryption) ([]byte, bool)
```
**Usage**
```go
data, _ := ioutil.ReadAll(ctx.Request().Body)
verifiedPlainPayload, err := crypto.Unmarshal(ecdsaPublicKey, data, nil)
ctx.Write(verifiedPlainPayload)
```
Or if data are encrypted and they should be **decrypted**:
```go
verifiedDecryptedPayload, err := crypto.Unmarshal(
ecdsaPublicKey,
data,
crypto.Decrypt(aesKey, nil),
)
```
## About encryption
The `Encrypt` package-level function returns an `Encryption` type value to be used on `Marshal`.
If "aesKey" is not empty then the "data" associated with the "additionalData" will be encrypted too.
If "aesKey" is not empty but "additionalData" is, then the `DefaultADATA` will be used to encrypt "data".
If "aesKey" is empty then encryption is disabled, the return value will be only signed.
```go
Encrypt(aesKey, additionalData []byte) Encryption
```
The `Decrypt` package-level function returns an `Decryption` type value to be used on `Unmarshal`.
If "aesKey" is not empty then the result will be decrypted.
If "aesKey" is not empty but "additionalData" is,
then the `DefaultADATA` will be used to decrypt the encrypted "data".
If "aesKey" is empty then decryption is disabled.
If `Marshal` had an `Encryption` then `Unmarshal` must have also.
```go
Decrypt(aesKey, additionalData []byte) Decryption
```
> Note that the `crypto.DefaultADATA` variable contains the default associated data used for `crypto.Encrypt` and `crypto.Decrypt` when "additionalData" is empty.
However, you can still use any custom encryption, the `crypto.Marshal and Unmarshal` functions accepts a `crypto.Encryption` and `crypto.Decryption` respectfully.
```go
// Encryption is the method signature when data
// should be signed and returned as encrypted.
type Encryption func(privateKey *ecdsa.PrivateKey, data []byte) ([]byte, error)
// Decryption is the method signature when data
// should be decrypted before signed.
type Decryption func(publicKey *ecdsa.PublicKey, data []byte) ([]byte, error)
```
### The crypto/gcm subpackage
The underline encryption implementation is done by the `iris/crypto/gcm` package.
We make use of the AES algorithm and the Galois/Counter Mode for our encryption/decription methods. Of course you can use the **`iris/crypto/gcm`** subpackage as standalone to other use cases inside your application as well.
The `gcm.MustGenerateKey` returns an AES key.
```go
MustGenerateKey() []byte
```
The `Encrypt` function of `gcm` subpackage encrypts and authenticates the plain data with any additional data and returns the ciphertext of it.
It uses the [AEAD](https://en.wikipedia.org/wiki/Authenticated_encryption) cipher mode providing authenticated encryption with associated
data.
The same additional data must be kept the same for `Decrypt`.
```go
Encrypt(aesKey, data, additionalData []byte) ([]byte, error)
```
The `Decrypt` function of the `gcm` subpackage decrypts and authenticates ciphertext, authenticates the additional data and, if successful, returns the resulting plain data.
The additional data must match the value passed to `Encrypt`.
```go
Decrypt(aesKey, ciphertext, additionalData []byte) ([]byte, error)
```
References:
- https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm
- https://apps.nsa.gov/iaarchive/library/ia-guidance/ia-solutions-for-classified/algorithm-guidance/suite-b-implementers-guide-to-fips-186-3-ecdsa.cfm
- https://www.nsa.gov/Portals/70/documents/resources/everyone/csfc/csfc-faqs.pdf
- https://en.wikipedia.org/wiki/Authenticated_encryption
--------
Finally it's time for some code.
Example Code:
Please read the _comments_ too for a better understanding.
```go
package main
import (
"io/ioutil"
"github.com/kataras/iris"
"github.com/kataras/iris/crypto"
)
var (
// Change that to your own key.
// Usually you have an ECDSA private key
// per identify, let's say a user, stored in a database
// or somewhere else and you use its public key
// to sign a user's payload and when this client
// wants to use this payload, on another route,
// you verify it comparing the signature of the payload
// with the user's public key.
//
// Use the crypto.MustGenerateKey to generate a random key
// or
// crypto.ParsePrivateKey to convert data or local file to an *ecdsa.PrivateKey.
// and `crypto.ParsePublicKey` if you only have access to the public one.
testPrivateKey = crypto.MustGenerateKey()
testPublicKey = &testPrivateKey.PublicKey
)
type testPayloadStructure struct {
Key string `json:"key"`
Value string `json:"value"`
}
// The Iris crypto package offers
// authentication (with optional encryption in top of) and verification
// of raw []byte data with `crypto.Marshal/Unmarshal` functions
// and JSON payloads with `crypto.SignJSON/VerifyJSON functions.
//
// Let's use the `SignJSON` and `VerifyJSON` here as an example,
// as this is the most common scenario for a web application.
func main() {
app := iris.New()
app.Post("/auth/json", func(ctx iris.Context) {
ticket, err := crypto.SignJSON(testPrivateKey, ctx.Request().Body)
if err != nil {
ctx.StatusCode(iris.StatusUnprocessableEntity)
return
}
// Send just the signature back
// ctx.WriteString(ticket.Signature)
// or the whole payload + the signature:
ctx.JSON(ticket)
})
app.Post("/verify/json", func(ctx iris.Context) {
var verificatedPayload testPayloadStructure // this can be anything.
// The VerifyJSON excepts the body to be a JSON structure of
// {
// "signature": the generated signature from /auth/json,
// "payload": the JSON client payload
// }
// That is the form of the `crypto.Ticket` structure.
//
// However, you are not limited to use that form, another common practise is to
// have the signature and the payload we need to check in the same string representation
// and for a better security you add encryption in top of it,
// so an outsider cannot understand what is what.
//
// Let's say for the shake of the example
// that the signature can be optionally provided by a URL ENCODED parameter
// and the request body is the payload without any encryption
// -
// of course you can pass an GCM type of encryption/decryption as Marshal's and
// Unmarshal's last input argument,
// see more about this at the iris/crypto/gcm subpackage for ready-to-use solutions.
// -
// So we will check if a url parameter is given, if so we will combine the signature and
// the body into one slice of bytes
// and we will make use of the `crypto.Unmarshal` instead of the `crypto.VerifyJSON`.
// -
if signature := ctx.URLParam("signature"); signature != "" {
payload, err := ioutil.ReadAll(ctx.Request().Body)
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
return
}
data := append([]byte(signature), payload...)
originalPayloadBytes, ok := crypto.Unmarshal(testPublicKey, data, nil)
if !ok {
ctx.Writef("this does not match, please try again\n")
ctx.StatusCode(iris.StatusUnprocessableEntity)
return
}
ctx.ContentType("application/json")
ctx.Write(originalPayloadBytes)
return
}
ok, err := crypto.VerifyJSON(testPublicKey, ctx.Request().Body, &verificatedPayload)
if err != nil {
ctx.Writef("error on verification: %v\n", err)
ctx.StatusCode(iris.StatusBadRequest)
return
}
if !ok {
ctx.Writef("this does not match, please try again\n")
ctx.StatusCode(iris.StatusUnprocessableEntity)
return
}
// Give back the verificated payload or use it.
ctx.JSON(verificatedPayload)
})
// 1.
// curl -X POST -H "Content-Type: application/json" -d\
// '{"key": "this is a key", "value": "this is a value"}'\
// http://localhost:8080/auth/json
// 2. The result will be something like this:
// {"payload":{"key":"this is a key","value":"this is a value"},\
// "signature":"UgXgbXXvs9nAB3Pg0mG1WR0KBn2KpD/xBIsyOv1o4ZpzKs45hB/yxXiGN1k4Y+mgjdBxP6Gg26qajK6216pAGA=="}
// 3. Copy-paste the whole result and do:
// curl -X POST -H "Content-Type: application/json" -d\
// '{"payload":{"key":"this is a key","value":"this is a value"},\
// "signature":"UgXgbXXvs9nAB3Pg0mG1WR0KBn2KpD/xBIsyOv1o4ZpzKs45hB/yxXiGN1k4Y+mgjdBxP6Gg26qajK6216pAGA=="}'\
// http://localhost:8080/verify/json
// 4. Or pass by ?signature encoded URL parameter:
// curl -X POST -H "Content-Type: application/json" -d\
// '{"key": "this is a key", "value": "this is a value"}'\
// http://localhost:8080/verify/json?signature=UgXgbXXvs9nAB3Pg0mG1WR0KBn2KpD%2FxBIsyOv1o4ZpzKs45hB%2FyxX
// iGN1k4Y%2BmgjdBxP6Gg26qajK6216pAGA%3D%3D
// 5. At both cases the result should be:
// {"key":"this is a key","value":"this is a value"}
// Otherise the verification failed.
//
// Note that each time server is restarted a new private and public key pair is generated,
// look at the start of the program.
app.Run(iris.Addr(":8080"))
}
```
You can read more examples and run testable code at the `iris/crypto`, `iris/crypto/sign` and `iris/crypto/gcm` packages themselves.

@ -44,7 +44,7 @@ ctx.ViewData("message", "Hello world!")
To bind a Go **model** to a view you have two options: To bind a Go **model** to a view you have two options:
- `ctx.ViewData("user", User{})` - variable binding as `{{.user.Name}}` for example - `ctx.ViewData("user", User{})` - variable binding as `{{.user.Name}}` for example
- `ctx.View("hello.html", User{})` - root binding as `{{.Name}}` for example. - `ctx.View("user-page.html", User{})` - root binding as `{{.Name}}` for example.
To **add a template function** use the `AddFunc` method of the preferred view engine. To **add a template function** use the `AddFunc` method of the preferred view engine.
@ -55,13 +55,13 @@ tmpl.AddFunc("greet", func(s string) string {
}) })
``` ```
To **reload on local file changes** call the view enginne's `Reload` method. To **reload on local file changes** call the view engine's `Reload` method.
```go ```go
tmpl.Reload(true) tmpl.Reload(true)
``` ```
To use **embedded** files and not depend on local file system use the [go-bindata](https://github.com/go-bindata/go-bindata) external tool and pass its `Asset` and `AssetNames` functions to the `Binary` method of the preferred view engine. To use **embedded** templates and not depend on local file system use the [go-bindata](https://github.com/go-bindata/go-bindata) external tool and pass its `Asset` and `AssetNames` functions to the `Binary` method of the preferred view engine.
```go ```go
tmpl.Binary(Asset, AssetNames) tmpl.Binary(Asset, AssetNames)

@ -14,6 +14,7 @@
* [[Wrap the Router|Routing-wrap-the-router]] * [[Wrap the Router|Routing-wrap-the-router]]
* [[Override Context|Routing-override-context]] * [[Override Context|Routing-override-context]]
* [[API Versioning]] * [[API Versioning]]
* [[Request Authentication]]
* [[File Server]] * [[File Server]]
* [[View]] * [[View]]
* [[Dependency Injection|dependency-injection]] * [[Dependency Injection|dependency-injection]]