Authentication service extension (Legacy)
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>
`