Authentication service extension (Legacy)

Authentication service extension (Legacy)

ℹ️
This topic refers to legacy configuration syntax. App gateways are now defined as Proxy apps. A new example of the authentication service extension is documented here.

Service Extensions allow complete flexibility over the process of establishing a user’s identity and managing it on a session.

How AppGateways Serve Traffic

A little context will be helpful to understand how the authentication works and where it fits into the identity flow of a user. Here is how traffic flows into an AppGateway (in pseudocode).

func Serve(appGateway *app.AppGateway, rw http.ResponseWriter, req *http.Request) {
  if !IsAuthenticated(){
    Authenticate(resp, req)
  }
  LoadAttrs()
  if IsAuthorized() {
    SetHeaders()
    Proxy()
  }
}

This is a simplification. The actual implementation caches the information about the session for efficiency, including the results of the authentication and authorization check. As such, each of these methods is not usually called for each request, but rather only for the first pass through for the user.

Authentication

As seen above in pseudocode, there are really two components to authentication. The Service Extension isAuthenticatedSE in yaml will check to see if a session is currently authenticated appropriately for a requested resource. If not, then the authenticateSE Service Extension in yaml handles the actual user interaction to authenticate the session. Notice that both extensions are under the authentication key.

ℹ️

AuthN vs. AuthZ

Authentication (AuthN) is used simply to assert the user’s identity, not to determine if they have access. A user may be authenticated, but may still be denied access at the Authorization (AuthZ) stage.

IsAuthenticated Service Extension

This Service Extension is relatively simple – it usually just checks for authentication on a user’s session.

Although this Service Extension is passed the http.ResponseWriter, typically it will not send any response directly to the user. Other extensions, such as authenticateSE, will handle user interaction.

Authenticate Service Extension

The AuthenticateSE Service Extension is run to determine user identity. It has access to fields and methods on the AppGateway struct that are provided to help facilitate authentication. For instance, the list of available IDPs can be used to hand off the authentication flow to a specific external authentication provider.

User Interaction

The authenticateSE Service Extension gives the capability to interact with the user in order to establish their identity. For example, the user may be redirected to an external authentication provider, or they may be presented a form prompting for their username and password. This means that sometimes the Service Extension may need to handle different requests; initially presenting the login page and then processing the POSTed username and password, or redirecting to Azure and then processing an OIDC token when the user is redirected back.

Examples

Authentication Against IDP

In this very basic example, the Service Extension is simply asking an IDP defined in the config to handle the Login of the user.

In this case the Azure IDP connector will handle the authentication necessary for the user’s session. After the user establishes their identity, the user will be redirected back to their originally requested URL, at which point the isAuthenticatedSE Service Extension above will return true indicating that they’ve been authenticated, and the user’s flow through the AppGateway will continue.

appgateways:
  - name: alpha
    # ...

    policies:
      - location: /
        authentication:
          isAuthenticatedSE:
            funcName: IsAuthenticated
            file: /etc/maverics/extensions/auth.go
          authenticateSE:
            funcName: Authenticate
            file: /etc/maverics/extensions/auth.go
        authorization:
          allowAll: true

/etc/maverics/extensions/auth.go

package main

import (
	"errors"
	"net/http"

	"maverics/app"
	"maverics/log"
	"maverics/session"
)

// IsAuthenticated determines if the user has been authenticated by Azure.
func IsAuthenticated(ag *app.AppGateway, rw http.ResponseWriter, req *http.Request) bool {
	log.Debug("msg", "determining if user is authenticated")

	if session.GetString(req, "azure.authenticated") == "true" {
		return true
	}

	return false
}

// Authenticate authenticates the user against Azure.
func Authenticate(ag *app.AppGateway, rw http.ResponseWriter, req *http.Request) error {
	log.Debug("msg", "authenticating user")

	azure, ok := ag.IDPs["azure"]
	if !ok {
		return errors.New("failed to find Azure IDP")
	}

	azure.CreateRequest().Login(rw, req)
	return nil
}

User Interaction

In this example, the user is presented a form to login if they are not already authenticated. The user’s credentials are collected and then validated against a local credential cache.

appgateways:
  - name: alpha
    # ...

    policies:
      - location: /
        authentication:
          isAuthenticatedSE:
            funcName: IsAuthenticated
            file: /etc/maverics/extensions/auth.go
          authenticateSE:
            funcName: Authenticate
            file: /etc/maverics/extensions/auth.go
        authorization:
          allowAll: true
package main

import (
	"errors"
	"fmt"
	"net/http"

	"maverics/app"
	"maverics/log"
	"maverics/session"
)

// IsAuthenticated determines if the user has been authenticated by Azure.
func IsAuthenticated(ag *app.AppGateway, rw http.ResponseWriter, req *http.Request) bool {
	log.Debug("msg", "determining if user is authenticated")

	if session.GetString(req, "se.authenticated") == "true" {
		return true
	}

	return false
}

// Authenticate authenticates the user by challenging the user for credentials.
func Authenticate(ag *app.AppGateway, rw http.ResponseWriter, req *http.Request) error {
	log.Debug("msg", "authenticating user")

	if req.Method != http.MethodPost && req.Method != http.MethodGet {
		return errors.New("received unexpected request")
	}

	if req.Method == http.MethodPost {
		err := req.ParseForm()
		if err != nil {
			return fmt.Errorf("failed to parse form: %w", err)
		}

		username := req.Form.Get("username")
		password := req.Form.Get("password")
		if username == "username" && password == "password" {
			log.Debug("msg", "successfully logged in user")
			session.Set(req, "se.authenticated", "true")
			http.Redirect(rw, req, "/", http.StatusFound)
			return nil
		}

		log.Debug(
			"msg", "user entered invalid credentials",
			"username", username,
		)
	}

	_, _ = fmt.Fprintf(rw, htmlForm)
	return nil
}

const htmlForm = `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
<form action="/sonar/reports" method="POST">
    <label for="username">Username:</label><br>
    <input type="text" id="username" name="username"><br>
    <label for="password">Password:</label><br>
    <input type="password" id="password" name="password"><br><br>
    <input type="submit" value="Submit">
</form>
</body>
</html>
`