User Authentication & Authorzation Progress | 221123


221123

_0620

User Authentication

So its been a while coming for this and a lot of items leading up to this, but I’ve been learning and working towards getting to be able to login with in the conduit project. I wanted to get some things down before I move forward since it has been a while and I feel like I have covered some ground, granted I am not there really yet.

Where I am at right now. I’ve got some micro-services up that do not a lot cause I don’t have users. I decided almost a year ago now that I wanted to use an open source identity/access manager and serve it myself. I do have that with keycloak and I have running locally. What I don’t have is a frontend(frontend client) nor a way for it to talk with the backend(which I kinda have) . I would like to go over my work on these two item.

Conduit Front End

So I was feeling froggy and decided to to explore some more options for the frontend. A few months ago I played with some Next.js, Deno and just react(I think there are some screenshot on older post). While shopping my options the internet kept hounding me to try Rust and I did getting a majority of the way through the Rust book.Then trying to combine a need and a want I thought why not use rust as front end which seems really odd at first, but Rust does do WebAssembly. Which in theory would allow me potentially move this away from the browser at some point in the future which I think would be preferable. There is a small problem though I really don’t know Rust, none the less I pushed on.

Yew Framework

I found a framework that use WASM to make web application called Yew. It’s kinda like react, but rust kinda… I invested in watching a long tutorial series on the framework. Which gave me a decent idea of how to work with it at the same time growing my confidences in rust. With that tutorial series done and Exploring examples on github I have some idea of how Yew works which can best be described as everything is a component.

Yew-OAuth2

Now back to the core idea for this note “User Authentication”. I say “Authentication” and not “User Authentication and Authorization” due to that I not really leaning on the OAuth so much. I mean I am little, but I mostly just want user to log with ODIC and check if there login is good(I will get in to this more later).

The internet found me a crate that would felicitate ODIC and OAuth2 and it came with an example that I am leaning on hard.

So this is where one my big learning events happened. The user client should not have the SSO/IAM client secret(keycloak client) in it. Since I am trying to make things secure(I know I am going to mess it up, but I am tryin). The idea I was using that I only want apps under my lock and key to be able to use my SSO, but after reading up on it(example). I was wrong and lost a week to this. The protection for the SSO comes from the Web Origins.

So this is what I have for the frontend.

Login

This leads this to this

Leads to this

Then back to this.

enter image description here

I know it’s not a looker, but I do now have a frontend that uses the SSO while handling the session, refreshing, logout/in.

So now I need to send that information to the backend. Since the SSO sends a JWT to the frontend I just need to pass that to backend to deal with and understand.

I created a simple request to send the JWT to the backend in the header of a request. I just function in component that loaded once a login is completed and boom it sends it to conduit backend.

async  fn  send_req(access_token: String ) ->  String {
	let  yeye  =  Request::new("http://localhost:9666/",);
	let  resp  =&yeye.method(gloo_net::http::Method::POST)
		.header("Authentication", &access_token)
		.send()
		.await
		.unwrap();

	return  String::from(String::from("Somthing"));
}

Conduit Backend

In backend i have created a new test/dev service using the same pattern I use for the rest of them.

I did have to add some extra CORS item to deal with sending JWT in the header to the backend. This did take some experimentation to get working.

I was getting pre-flight errors on the frontend due to CORS so i needed to added some things to header to allow for. In looking into this I learn that these thing are just added to the response header that checked in the pre-flight(ie ““Access-Control-Allow-Origin”, ““Access-Control-Allow-Methods”) which was something new I learned.

	...
	credentials := gohandlers.AllowCredentials()  
	origins := gohandlers.AllowedOrigins([]string{"http://localhost:8080"})  
	headers := gohandlers.AllowedHeaders([]string{"Authentication"})  
	methods := gohandlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "OPTIONS"})
	...

	...
srv := &http.Server{  
   Addr:         ":9666", // configure the bind address  
   Handler:      gohandlers.CORS(credentials, methods, origins, headers)(router),
  ...

Now I have the access-token in the backend. There are few things I can do with it. I have successfully done one them, but the most important one.

Token Introspection

The Token Introspection extension defines a mechanism for resource servers to obtain information about access tokens. With this spec, resource servers can check the validity of access tokens, and find out other information such as which user and which scopes are associated with the token. ~ OAuth.net

With the token now I need to check the token with the SSO to confirm it came from it and that its still valid which is another HTTP request.

which is this

curl -L -X POST 'http://{HOST/IP_ADDRESS}:{PORT}/realms/{REALM}/protocol/openid-connect/token/introspect' \
   -H "Authorization: Basic base64<CLIENT_ID:CLIENT_SECRET>" \
   -H "Accept: application/json" \
   -H "Content-Type: application/x-www-form-urlencoded" \
   --data-urlencode 'token=<ACCESS_TOKEN>' \
   | jq

Let’s break this down.

Its a POST request with some headers, and json body with the access token in it.

The destination Keycloak will tell you at its ”…/.well-known/openid-configuration” mine locally was ‘http:/ /keycloak.test/realms/gatehouse/protocol/openid-connect/token/introspect’

Authorization is base64ing the CLIENT_ID:CLIENT_SECRET. This is where I learned something else that after being suck and just trying it. I made a new SSO Client that was not public in the same realm thinking it should have the same access the other client for the frontend being in the same realm and with the same roles.

To get the base64 of that new client and its secret echo pipe it into base64 and copy paste that into the HTTP request

echo -n 'CLIENT_ID:CLIENT_SECRET' | base64 

The other two headers are as they read.

Then just insert the access token into that CURL to get your introspective if all works well you get something like this. I added some comments to give myself a better understanding

{  
  "exp": 1669201864, // Expiration time  
  "iat": 1669201564, // Issused at  
  "auth_time": 1669201564, // Time Authentication Happened (All time's Unix)  
  "jti": "cc18de55-90d4-4730-b326-2dc64612cd6c", //JWT Unique ID  
  "iss": "http://keycloak.test/realms/gatehouse", // Issuer  
  "aud": "account", // Intended Token Audince  
  "sub": "8b603c06-ee77-4f60-a0bd-72652808b861", //Subject ID (UUID) or the users UUID in the  
  "typ": "Bearer", //Token Type  
  "azp": "dev-conduit-rust", //Authorization Party (Client)  
  "nonce": "Y56MSoeXjA0IxWv1mmWEDA", //Unique Value given to Token ID  
  "session_state": "7597c0bb-28cf-4016-926f-9869d01e2ea9", //Unique Value given to Session  
  "name": "Test User 0000",  
  "given_name": "Test User",  
  "family_name": "0000",  
  "preferred_username": "test-user-0000",  
  "acr": "1", //Authencation Context Id  
  "allowed-origins": [ // Where request can come from( the :9666 isn't needed)  
  "http://localhost:8080",  
  "http://localhost:9666"  
  ],  
  "realm_access": {  
    "roles": [  
      "offline_access", // Still not postive what this its a client role in keyvcloak more information is needed  
  "default-roles-gatehouse",  
  "uma_authorization"  
  ]  
  },  
  "resource_access": {  
    "account": {  
      "roles": [  
        "manage-account",  
  "manage-account-links",  
  "view-profile"  
  ]  
    }  
  },  
  "scope": "openid microprofile-jwt profile",  
  "sid": "7597c0bb-28cf-4016-926f-9869d01e2ea9", //Session ID  
  "upn": "test-user-0000",  
  "groups": [  
    "offline_access",  
  "default-roles-gatehouse",  
  "uma_authorization"  
  ],  
  "client_id": "dev-conduit-rust",  
  "username": "test-user-0000",  
  "active": true // The most importatnt part of the introspective Its if the Token is active and good to go  
}

The most import part is the active:true at the bottom that tells me the token is good to go and active.

Now I just have to convert that into go

func tokenIntrospect(accessToken *string) (*http.Response, error) {  
   introspectURL := "http://keycloak.test/realms/gatehouse/protocol/openid-connect/token/introspect"  
  
  //Getting the AccessToken into JSON 
  type Payload struct {  
      Token string `json:"token"`  
  }
   formData := &Payload{  
      Token: *accessToken,  
  }  
  // Getting the Toke to URL Values
   payload := url.Values{  
      "token": {formData.Token},  
  }  
   req, err := http.NewRequest("POST", introspectURL, strings.NewReader(payload.Encode()))  
   if err != nil {  
      log.Printf("[ERROR] Request Generator failed: %#v", err)  
      return &http.Response{}, err  
   }  
   req.Header.Add("Authorization", "Basic ZGV[REDACTED]==")  
   req.Header.Set("Accept-Encoding", "application/json")  
   req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
   // Not postive if this still needed,  
   req.Header.Add("Content-Length", strconv.Itoa(len(payload.Encode())))  
   client := &http.Client{  
      Timeout: time.Second * 11,  
  }  
   
   // Sending the request for the introspective.  
  res, err := client.Do(req)  
  
   if err != nil {  
      log.Printf("[ERROR] Sending Token Introspect Request: %#v", err)  
      return &http.Response{}, err  
   }  
   defer func(Body io.ReadCloser) {  
      err := Body.Close()  
      if err != nil {  
         log.Printf("[ERROR] Closing Body?!? %#v", err)  
      }  
   }(res.Body)     
  
   return res, nil

200 response for the token introspective This is still very basic and just a function. I need to make this in a Middle ware that every service use will use. I will have also have to update my Saga Pattern works to internal services to pass this along, and find to add this into the Kafka implementation of the Saga Pattern even tho no services use it currently

Missing token features

I am still having an issues with another feature of the token and that is local token verification since the token was created with a key with public SSO client I am not positive how how to get the key to work( I think there is still some gaps in my knowledge

Looking it at again when writing this I figured it out. It was something really dumb. (god this feelz good)

It was cause I did not add this shit to the start and finish to the public RSA key. God this was stupid. I just assumed this was remove…

“—–BEGIN CERTIFICATE—–\n …. “\n—–END CERTIFICATE—–”

Okay know I can check locally on the token sent is legit, but not still valid cause if the user logs out before this expires.

func localJWTVerification(accessToken *string) error {  
  
   //log.Printf("TokenString: %#v", *accessToken)  
  
  RSASampleSecret := "-----BEGIN CERTIFICATE-----\n" + "MII[REDACTED]AB" + "\n-----END CERTIFICATE-----"  
  key, er := jwt.ParseRSAPublicKeyFromPEM([]byte(RSASampleSecret))  
   if er != nil {  
      fmt.Println(er)  
   }  
   token, err := jwt.Parse(*accessToken, func(token *jwt.Token) (interface{}, error) {  
      // Don't forget to validate the alg is what you expect:  
  if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {  
         return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])  
      }  
  
      return key, nil  
  })  
  
   if token.Valid {  
      fmt.Println("Token is valid!!!!")  
      return nil  
  } else if errors.Is(err, jwt.ErrTokenMalformed) {  
      fmt.Println("That's not even a token")  
      return ErrLocalTokenValidationFailed  
   } else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {  
      fmt.Println("Timing is everything")  
      return ErrLocalTokenValidationFailed  
   } else {  
      fmt.Println("Couldn't handle this token:", err)  
      return ErrLocalTokenValidationFailed  
   }  
}

Now What.

I am glad of what I have done it been a pain just being stuck for days on dumb things with a 100 tabs open.

Now I need to data map the user itself, intergrate it in the other services. figure out how to deal with new user and what to do with the user when they login. Plan a system of sending data to and frow rust and go.

Forward…