Update documentation for TLS and authentication

Change-Id: Iac283b4feb17b5a9ddaf4d50d4fa53b77d29a4a3
This commit is contained in:
Schiefelbein, Andrew 2020-09-03 14:22:23 -05:00
parent 172be5b722
commit f59f2bc6eb
8 changed files with 90 additions and 390 deletions

View File

@ -47,19 +47,11 @@ GO_FLAGS := -ldflags=$(LD_FLAGS) -trimpath
BUILD_DIR := bin
# Find all main.go files under cmd, excluding airshipui itself
EXAMPLE_NAMES := $(notdir $(subst /main.go,,$(wildcard examples/*/main.go)))
EXAMPLES := $(addprefix $(BUILD_DIR)/, $(EXAMPLE_NAMES))
MAIN := $(BUILD_DIR)/airshipui
EXTENSION :=
ifdef XDG_CONFIG_HOME
OCTANT_PLUGINSTUB_DIR ?= ${XDG_CONFIG_HOME}/octant/plugins
# Determine if on windows
else ifeq ($(OS),Windows_NT)
OCTANT_PLUGINSTUB_DIR ?= $(subst \,/,${LOCALAPPDATA}/octant/plugins)
ifeq ($(OS),Windows_NT)
EXTENSION=.exe
else
OCTANT_PLUGINSTUB_DIR ?= ${HOME}/.config/octant/plugins
endif
DIRS = internal

View File

@ -15,12 +15,14 @@ to go to a separate url or application.
git clone https://opendev.org/airship/airshipui
cd airshipui
make # Note running behind a proxy can cause issues, notes on solving is in the Appendix of the Developer's Guide
bin/airshipui
```
Once AirshipUI has started you should be able to browse to it at https://localhost:10443
## Adding Additional Functionality
Airship UI can be seamlessly integrated with service dashboards and other web-based tools by providing the necessary
configuration in $HOME/.airship/airshipui.json.
configuration in etc/airshipui.json.
To add service dashboards, create a section at the top-level of airshipui.json as follows:

BIN
docs/img/authentication.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -10,7 +10,6 @@ Clone the Airship UI repository and build.
git clone https://opendev.org/airship/airshipui
cd airshipui
make # Note running behind a proxy can cause issues, notes on solving is in the Appendix
make examples # (optional)
**NOTE:** Make will install node.js-v12.16.3 into your tools directory and will use that as the node binary for the UI
building, testing and linting. For windows this can be done using [cygwin](https://www.cygwin.com/) make.
@ -19,41 +18,51 @@ Run the airshipui binary
./bin/airshipui
## Authentication
## Security
### Pluggable authentication methods
The AirshipUI is not designed to create authentication credentials but to have them supplied to it either by a
configuration or by an external entity. The expectation is that there will be an external URL that will handle
authentication for the system which may need to be modified or created. The endpoint will need to be able to
forward a
[bearer token](https://oauth.net/2/bearer-tokens/),
[basic auth](https://en.wikipedia.org/wiki/Basic_access_authentication)
or cookie data to the Airship UI backend service.
### Transport Layer Security
The UI will need to send sensitive / receive information therefore all channels of communication will need to be encrypted. The main protocol for this is [HTTPS](https://en.wikipedia.org/wiki/HTTPS), and the websocket communication is also over the secured channel ([wss](https://tools.ietf.org/html/rfc6455#page-55))
To configure the pluggable authentication the following must be added to the $HOME/.airshipui/airshipui.json file:
The airshipui stores the public and private key location in the etc/airshipui.json by default. If one is not present at the time of the airshipui is started a self signed certificate and private key will be generated and stored in etc/airshipui.json for the server to start in a developer's mode. This will cause an SSL error on your browser that
you will need to click past to get to the UI. It is assumed the server will have access to the proper key & certificate in production. Both the private key and certificate need to be ASCII PEM formatted.
"authMethod": {
"url": "<protocol>://<host:port>/<path>/<method>"
Example webservice definition in etc/airshipui.json:
```
"webservice": {
"host": "<host, default is localhost>",
"port": <port, default is 10443>,
"publicKey": "<path>/<cert>.pem",
"privateKey": "<path>/<private_key>.pem"
},
```
### User Authentication
The UI uses Json Web Tokens ([JWT](https://tools.ietf.org/html/rfc7519)) to control access to the UI. The default method of generation is based on a userid and password enforcement
that the user is required to enter the first time accessing the UI. The UI will store the token locally and use it to authenticate the communication with the backend on every
transaction.
The airshipui stores the user and password in etc/airshipui.json by default. The userid is clear text but the password is an non reversible sha512 hash of the password which is used to compare the supplied password with the expected password. No clear text passwords are stored.
If no id and password is supplied the airshipui will create a default userid and password and store it in the etc/airshipui.json file so there will be no ability to use the UI
without a base id / password challenge authentication.
The default userid is: admin The default password is: admin
To generate a password you can run the password.go program in the tools directory:
```
$ go run tools/password.go test_password
c8afeec4e9d29fa6307bc246965fe136a95bc47a9cfdedba0df256358eaa45ec0bf8d7a4333a4b13dc9a5508137d0f4d212272b27e64e41d4745a66b5f480759
```
Example user definition in etc/airshipui.json:
```
"users": {
"test": "c8afeec4e9d29fa6307bc246965fe136a95bc47a9cfdedba0df256358eaa45ec0bf8d7a4333a4b13dc9a5508137d0f4d212272b27e64e41d4745a66b5f480759"
}
```
After the user is defined in the etc/airshipui.json file the user can be used for authentication going forward.
Note: By default the system will start correctly without any authentication urls supplied to the configuration.
The expectation is that AirshipUI will be running in a minimal least authorized configuration.
### Example Auth Server
There is an example authentication server in examples/authentication/main.go. These endpoints can be added to the
$HOME/.airshipui/airshipui.json and will allow the system to show a basic authentication test.
1. Basic auth on http://127.0.0.1:12321/basic-auth
2. Cookie based auth on http://127.0.0.1:12321/cookie
3. OAuth JWT (JSON Web Token) on http://127.0.0.1:12321/oauth
To start the system cd to the root of the AirshipUI repository and execute:
go run examples/authentication/main.go
#### Example Auth Server Credentials
+ The example auth server id is: **airshipui**
+ The example auth server password is: **Open Sesame!**
### Authentication decision tree
![AirshipUI Interactions](../img/authentication.jpg "AirshipUI Authentication Decision Tree")
## Behind the scenes

View File

@ -1,262 +0,0 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/json"
"io"
"io/ioutil"
"log"
"net/http"
"text/template"
"time"
"github.com/dgrijalva/jwt-go"
)
// page struct is used for templated HTML
type page struct {
Title string
}
// id and password passed from the test page
type authRequest struct {
ID string `json:"id,omitempty"`
Password string `json:"password,omitempty"`
}
func main() {
// we're not picky, so we'll take everything and sort it out later
http.HandleFunc("/", handler)
log.Println("Example Auth Server listening on :12321")
err := http.ListenAndServe(":12321", nil)
if err != nil {
log.Fatal(err)
}
}
// URI check for /basic-auth, /cookie and /oauth, everything else gets a 404
// Also a switch for GET and POST, everything else gets a 415
func handler(w http.ResponseWriter, r *http.Request) {
method := r.Method
uri := r.RequestURI
if uri == "/basic-auth" || uri == "/cookie" || uri == "/oauth" {
switch method {
case http.MethodGet:
get(uri, w)
case http.MethodPost:
post(uri, w, r)
default:
w.WriteHeader(http.StatusNotFound)
log.Printf("Method %s for %s being rejected, not implemented", method, uri)
}
} else {
w.WriteHeader(http.StatusNotFound)
log.Printf("URI %s being rejected, not found", uri)
}
}
// handle the GET function and return a templated page
func get(uri string, w http.ResponseWriter) {
var p page
switch uri {
case "/basic-auth":
p = page{
Title: "Basic Auth",
}
case "/cookie":
p = page{
Title: "Cookie",
}
case "/oauth":
p = page{
Title: "OAuth",
}
}
if p != (page{}) {
// parse and merge the template
err := template.Must(template.ParseFiles("./examples/authentication/templates/index.html")).Execute(w, p)
if err != nil {
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
log.Printf("Error getting the templated html: %v", err)
http.Error(w, "Error getting the templated html", http.StatusInternalServerError)
}
}
}
// handle the POST function and return a mock authentication
func post(uri string, w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading body: %v", err)
http.Error(w, "can't read body", http.StatusBadRequest)
return
}
var authAttempt authRequest
err = json.Unmarshal(body, &authAttempt)
if err == nil {
// TODO: make the id and password part of a conf file somewhere
id := authAttempt.ID
passwd := authAttempt.Password
if id == "airshipui" && passwd == "Open Sesame!" {
w.WriteHeader(http.StatusCreated)
response := map[string]interface{}{
"id": id,
"name": "Some Name",
"expiration": time.Now().Add(time.Hour * 24).Unix(),
}
switch uri {
case "/basic-auth":
response["X-Auth-Token"] = base64.StdEncoding.EncodeToString([]byte(id + ":" + passwd))
response["type"] = "basic-auth"
postHelper(response, w)
case "/cookie":
response["type"] = "cookie"
cookieHandler(response, w)
case "/oauth":
response["type"] = "oauth"
jwtHandler(id, passwd, response, w)
}
} else {
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
http.Error(w, "Bad id or password", http.StatusUnauthorized)
}
} else {
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
log.Printf("Error unmarshalling the request: %v", err)
http.Error(w, "Error unmarshalling the request", http.StatusBadRequest)
}
}
// potentially more complex logic happens here with cookie data
func cookieHandler(response map[string]interface{}, w http.ResponseWriter) {
cookie, err := json.Marshal(response)
if err != nil {
log.Printf("Error marshaling cookie response: %v", err)
}
b, err := encrypt(cookie)
if err != nil {
log.Printf("Error encrypting cookie response: %v", err)
postHelper(nil, w)
} else {
response["cookie"] = b
postHelper(response, w)
}
}
// potentially more complex logic happens here with JWT data
func jwtHandler(id string, passwd string, response map[string]interface{}, w http.ResponseWriter) {
token, err := createToken(id, passwd)
if err != nil {
log.Printf("Error creating JWT token: %v", err)
postHelper(nil, w)
} else {
response["jwt"] = token
postHelper(response, w)
}
}
// Helper function to reduce the number of error checks that have to happen in other functions
func postHelper(returnData map[string]interface{}, w http.ResponseWriter) {
if returnData == nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
} else {
log.Printf("Auth data %s\n", returnData)
b, err := json.Marshal(returnData)
if err != nil {
log.Printf("Error marshaling the response: %v", err)
http.Error(w, "Internal error", http.StatusInternalServerError)
} else {
_, err := w.Write(b)
if err != nil {
log.Printf("Error sending POST response to client: %v", err)
} else {
go notifyElectron(b)
}
}
}
}
// This is intended to send an auth completed message to the system so that it knows there was a successful login
func notifyElectron(data []byte) {
// TODO: probably need to pull the electron url out into its own
resp, err := http.Post("http://localhost:8080/auth", "application/json; charset=UTF-8", bytes.NewBuffer(data))
if err != nil {
log.Printf("Error sending auth complete to electron. The response is %v, the error is %v\n", resp, err)
}
}
// aes requires a 32 byte key, this is random for demo purposes
func randBytes(length int) ([]byte, error) {
b := make([]byte, length)
_, err := rand.Read(b)
if err != nil {
return nil, err
}
return b, nil
}
// this creates a random ciphertext for demo purposes
// this is not intended to be reverseable or to be used in production
func encrypt(data []byte) ([]byte, error) {
b, err := randBytes(256 / 8)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(b)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
ciphertext := gcm.Seal(nonce, nonce, data, nil)
return ciphertext, nil
}
// create a JWT (JSON Web Token) for demo purposes, this is not to be used in production
func createToken(id string, passwd string) (string, error) {
// create the token
token := jwt.New(jwt.SigningMethodHS256)
// set some claims
claims := make(jwt.MapClaims)
claims["username"] = id
claims["password"] = passwd
claims["exp"] = time.Now().Add(time.Hour * 24).Unix()
token.Claims = claims
//Sign and get the complete encoded token as string
return (token.SignedString([]byte("airshipui")))
}

View File

@ -1,86 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>AirshipUI Test {{.Title}}</title>
<link rel="icon" href="data:;base64,=">
</head>
<script>
function testIt() {
document.getElementById("AuthBtn").disabled = true;
console.log(window.location.pathname);
let xhr = new XMLHttpRequest();
xhr.open("POST", window.location.pathname);
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhr.onload = function () {
if (this.status === 201) {
document.cookie = "airshipUI=" + xhr.response + "expires=" + new Date().getUTCDate;
console.log(JSON.parse(xhr.response));
} else {
console.log({
status: this.status,
statusText: xhr.statusText
});
document.getElementById("OutputDiv").innerHTML = '<span style="color:red"><h2>&#9760; ID or Password is incorrect please try again &#9760;</h2></span>';
document.getElementById("AuthBtn").disabled = false;
}
};
xhr.onerror = function () {
reject({
status: this.status,
statusText: xhr.statusText
});
};
let json = JSON.stringify({"id": document.getElementById("ID").value, "password": document.getElementById("Passwd").value});
console.log(json)
xhr.send(json);
}
</script>
<body>
<h1>Airship UI Test {{.Title}}</h1>
<table>
<tr>
<td>
<b>Id:</b>&nbsp;&nbsp;
</td>
<td>
<input type="text" id="ID">
</td>
</tr>
<tr>
<td>
<b>Password:</b>&nbsp;&nbsp;
</td>
<td>
<input type="password" id="Passwd">
</td>
</tr>
<tr>
<td colspan="2">
<button id="AuthBtn" onclick="testIt()">Test It!</button>
</td>
</tr>
</table>
<div id="OutputDiv"></div>
<h2>&#9888; Warning! &#9888;</h2>
<p>This is a {{.Title}} test page is only intended as an example for how to use {{.Title}} with AirshipUI.</p>
<p>The System will return the following HTML status codes and responses</p>
<ul>
{{if eq .Title "Basic Auth"}}
<li>201: Created. The password attempt was successful and the backend has sent an xauth token header to AirshipUI.</li>
{{else if eq .Title "Cookie"}}
<li>201: Created. The password attempt was successful and the backend has set a cookie and sent the cookie contents to AirshipUI.</li>
{{else if eq .Title "OAuth"}}
<li>201: Created. The password attempt was successful and the backend has set a JWT (JSON Web Token) and sent the JWT contents to AirshipUI.</li>
{{end}}
<li>400: Bad request. There was an error sending the system the authentication request, most likely bad JSON.</li>
<li>401: Unauthorized. Bad id / password attempt.</li>
<li>403: Forbidden. The id / password combination was correct but the id is not allowed for the resource.</li>
<li>500: Internal Server Error. There was a processing error on the back end.</li>
</ul>
</body>
</html>

1
go.mod
View File

@ -3,7 +3,6 @@ module opendev.org/airship/airshipui
go 1.13
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/google/uuid v1.1.1
github.com/gorilla/websocket v1.4.2
github.com/pkg/errors v0.9.1

46
tools/password.go Executable file
View File

@ -0,0 +1,46 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"crypto/sha512"
"fmt"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "go run password.go <password>",
Short: "Create an sha512 password hash",
Long: "This creates an sha512 password hash used for user authentication in the etc/airshipui.json conf file",
Run: launch,
}
// take the password argument and turn it into a hash
func launch(cmd *cobra.Command, args []string) {
if len(args) == 1 {
// create and disply the sha512 hash for the password
hash := sha512.New()
hash.Write([]byte(args[0]))
fmt.Printf("%x\n", hash.Sum(nil))
} else {
fmt.Println("There should be 1 password argument")
}
}
func main() {
rootCmd.Execute()
}