...
Code Block | ||||
---|---|---|---|---|
| ||||
package main import ( "context" "encoding/json" "fmt" "github.com/open-policy-agent/opa/rego" "io/ioutil" "net/http" "net/url" "os" ) type Jwttoken struct { Access_token string Expires_in int Refresh_expires_in int Refresh_token string Token_type string Not_before_policy int Session_state string Scope string } var token Jwttoken var opaPolicy string = ` package authz import future.keywords.in default allow = false jwks := jwks_request("http://keycloak:8080/auth/realms/opa/protocol/openid-connect/certs").body filtered_jwks := [ key | some key in jwks.keys key.use == "sig" ] token_cert := json.marshal({"keys": filtered_jwks}) token = { "isValid": isValid, "header": header, "payload": payload } { [isValid, header, payload] := io.jwt.decode_verify(input, { "cert": token_cert, "aud": "account", "iss": "http://keycloak:808 0/auth/realms/opa"}) } allow { is_token_valid } is_token_valid { token.isValid now := time.now_ns() / 1000000000 token.payload.iat <= now now < token.payload.exp token.payload.clientRole = "[opa-client-role]" } jwks_request(url) = http.send({ "url": url, "method": "GET", "force_cache": true, "force_json_decode": true, "force_cache_duration_seconds": 3600 # Cache response for an hour }) ` func getToken() string { clientSecret := "63wkv0RUXkp01pbqtNTSwghhTxeMW55I" clientId := "opacli" realmName := "opa" keycloakHost := "keycloak" keycloakPort := "8080" keycloakUrl := "http://" + keycloakHost + ":" + keycloakPort + "/auth/realms/" + realmName + "/protocol/openid-connect/token" resp, err := http.PostForm(keycloakUrl, url.Values{"client_secret": {clientSecret}, "grant_type": {"client_credentials"}, "client_id": {clientId}}) if err != nil { fmt.Println(err) panic("Something wrong with the credentials or url ") } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) json.Unmarshal([]byte(body), &token) return token.Access_token } func traceOpa(input string) { ctx := context.TODO() test := rego.New( rego.Query("x = data.authz.allow"), rego.Trace(true), rego.Module("example.rego", opaPolicy), rego.Input(input), ) test.Eval(ctx) rego.PrintTraceWithLocation(os.Stdout, test) } func evaluateOpa(input string) { ctx := context.TODO() query, err := rego.New( rego.Query("x = data.authz.allow"), rego.Module("example.rego", opaPolicy), ).PrepareForEval(ctx) if err != nil { // Handle error. fmt.Println(err.Error()) } results, err := query.Eval(ctx, rego.EvalInput(input)) // Inspect results. if err != nil { // Handle evaluation error. fmt.Println("Error: " + err.Error()) } else if len(results) == 0 { // Handle undefined result. fmt.Println("Results are empty") } else { // Handle result/decision. fmt.Printf("Results = %+v\n", results) //=> [{Expressions:[true] Bindings:map[x:true]}] } } func main() { tokenStr := getToken() traceOpa(tokenStr) evaluateOpa(tokenStr) } |
We can combined OPA bundles with dynamic composition to provide different policies for differect services.
In the root directory of your bundle create main.rego
Code Block | ||||
---|---|---|---|---|
| ||||
package main
import input.attributes.request.http as http_request
name := trim_prefix(replace(http_request.path, "-", ""), "/")
router[policy] = data.policies[name][policy].deny
deny[msg] {
policy := router[_]
msg := policy[_]
}
allow {
count(deny) == 0
} |
This main policy will use the request path to determine which policy to apply. (We remove the forward slash and and "-" characters)
Create a directory policies/<app name> off the main directory.
In here create a policy called policy.rego
Code Block | ||||
---|---|---|---|---|
| ||||
package policies.rappopaprovider.policy
import input.attributes.request.http as http_request
import future.keywords.in
jwks := jwks_request("http://keycloak:8080/auth/realms/opa/protocol/openid-connect/certs").body
filtered_jwks := [ key |
some key in jwks.keys
key.use == "sig"
]
token_cert := json.marshal({"keys": filtered_jwks})
token = { "isValid": isValid, "header": header, "payload": payload } {
[_, encoded] := split(http_request.headers.authorization, " ")
[isValid, header, payload] := io.jwt.decode_verify(encoded, { "cert": token_cert, "aud": "account", "iss": "http://keycloak:8080/auth/realms/opa"})
}
deny[msg] {
not is_token_valid
msg = "denied by rappopaprovider.policy: not a valid token"
}
is_token_valid {
token.isValid
now := time.now_ns() / 1000000000
token.payload.iat <= now
now < token.payload.exp
token.payload.clientRole = "[opa-client-role]"
}
jwks_request(url) = http.send({
"url": url,
"method": "GET",
"force_cache": true,
"force_json_decode": true,
"force_cache_duration_seconds": 3600 # Cache response for an hour
}) |
This policy will verify the jwt token, check the token issue time and expiration time against the current time and also ensure the token contains the correct role.
If all these condtions are met th user will be granted access to the resource.
NGINX can be used for the bundles server: nginx.yaml
OPA server: opa.yaml
rapp-opa-provider: rapp-opa-provider.yaml