Hey, I have been trying to integrate hydra and kra...
# talk-kratos
d
Hey, I have been trying to integrate hydra and kratos but running into an error, whenever I try to redirect to login UI with
return_to
param set to loginchallelnge generate by hydra, the login flow is not as expected, Were as if I hit the endpoint
localhost:4433/self-service/login/browser
I get the appropriate response (second image), I also checked the response of
/self-service/login/flows?id=<flowid>
I get
403
Unauthorised access error. been referencing https://github.com/atreya2011/go-kratos-test/tree/hydra
b
@dazzling-honey-93005 can you switch to the
hydra-consent
branch and use the code in that branch. The README in that branch is up to date to get everything up and running 🙏🏼
d
@bulky-architect-22083 I tried that as well, getting same error. Also code in
hydra
and
hydra-consent
branch is not different ig.
b
Actually the code is different in both branches... Did you try following all the steps in the README?
d
Yes, when I run the example (the code directly in branch) everything works but when I am implementing on my project things don’t work (above error)
b
Ah I see, in which case, then the flow chart in the readme maybe helpul!
d
What I am doing is same as what is done in
HandleLogin
handler in the example code, What happens is login challenge is created and then login flow is created, I get the
flowID
and then I call
GetSelfServiceLoginFlow
which will contain all the UI attributes to populate the form, but it is empty. If the flowID is generated correctly, how ``GetSelfServiceLoginFlow`` returns wrong response. I can confirm
identity schema
is not malformed, as when I explicitly call
localhost:4433/self-service/login/browser
I get the correct form
b
I would be able to help if I can see the code
d
make reciepe for hydra:
Copy code
hydra-dev:
	(docker rm -f ory-hydra) || true 
	docker run -it --rm \
		--network meshery-net \
		oryd/hydra:v1.3.2 \
		migrate sql --yes $(DEV_DB_URL_HYDRA)
	docker run -d \
		--network meshery-net \
		--name ory-hydra \
		-p $(DEV_HYDRA_PORT_ADMIN):4445 \
		-p $(DEV_HYDRA_PORT_PUBLIC):4444 \
		-e DEV_SECRETS_SYSTEM=$(DEV_SECRETS_SYSTEM) \
		-e DSN=$(DEV_DB_URL_HYDRA) \
		-e URLS_SELF_ISSUER=$(DEV_HYDRA_PUBLIC) \
		-e URLS_CONSENT=$(DEV_SERVER_BASE_URL)/consent \
		-e URLS_LOGIN=$(DEV_SERVER_BASE_URL)/login \
		-e OAUTH2_ERROR_URL=$(DEV_SERVER_BASE_URL)/error \
		-e OAUTH2_ACCESS_TOKEN_STRATEGY=jwt \
		-e TTL_ACCESS_TOKEN=$(DEV_TTL_ACCESS_TOKEN) \
		-e TTL_REFRESH_TOKEN=$(DEV_TTL_REFRESH_TOKEN) \
		oryd/hydra:v1.3.2 serve all --dangerous-force-http
	docker run --rm -it \
		--network meshery-net \
		-e HYDRA_ADMIN_URL=<http://ory-hydra:4445> \
		oryd/hydra:v1.3.2 \
		clients create \
			--id $(DEV_OAUTH2_CLIENT_ID) \
			--secret $(DEV_OAUTH2_SECRET) \
			--grant-types authorization_code,refresh_token,client_credentials,implicit \
			--response-types token,code,id_token \
			--scope openid,offline,offline_access \
			--callbacks $(DEV_SERVER_BASE_URL)/callback
kratos config:
Copy code
version: v0.10.1
dsn: <postgres://postgres:postgres@postgres:5432/kratos?sslmode=disable>

serve:
  public:
    base_url: <http://127.0.0.1:9010/>
    cors:
      enabled: true
  admin:
    base_url: <http://127.0.0.1:9011/>

selfservice:
  default_browser_return_url: <http://127.0.0.1:9876/>
  allowed_return_urls:
    - <http://127.0.0.1:9876>

  methods:
    password:
      enabled: true

    link:
      enabled: true
      config:
        lifespan: 15m

  flows:
    error:
      ui_url: <http://127.0.0.1:9876/error>

    settings:
      ui_url: <http://127.0.0.1:9876/settings>
      privileged_session_max_age: 15m

    recovery:
      enabled: true
      ui_url: <http://127.0.0.1:9876/recovery>

    verification:
      enabled: true
      ui_url: <http://127.0.0.1:9876/verification>
      after:
        default_browser_return_url: <http://127.0.0.1:9876/>
      lifespan: 15m

    logout:
      after:
        default_browser_return_url: <http://127.0.0.1:9876/login>

    login:
      ui_url: <http://127.0.0.1:9876/login>
      lifespan: 10m

    registration:
      lifespan: 10m
      ui_url: <http://127.0.0.1:9876/registration>
      after:
        password:
          hooks:
            - hook: session
        oidc: 
          hooks: 
            - hook: session
log:
  level: debug
  format: text
  leak_sensitive_values: true

secrets:
  cookie:
    - qwertyuiop
  cipher:
    - qwertyiplkjhgfdsxcvbbn

ciphers:
  algorithm: xchacha20-poly1305

hashers:
  argon2:
    parallelism: 1
    memory: 128MB
    iterations: 2
    salt_length: 16
    key_length: 16

identity:
  schemas:
    - id: default
      url: file:///home/ory/identity.schema.json
  default_schema_id: "default"
  
courier:
  smtp:
    connection_uri: <smtps://test:test@mailslurper:1025/?skip_ssl_verify=true>
oauth config when starting the server:
Copy code
&oauth2.Config{
			ClientID:     viper.GetString("OAUTH2_CLIENT_ID"),
			ClientSecret: viper.GetString("OAUTH2_SECRET"),
			Endpoint: oauth2.Endpoint{
				TokenURL:  viper.GetString("HYDRA_PUBLIC") + "/oauth2/token",
				AuthURL:   viper.GetString("HYDRA_PUBLIC") + "/oauth2/auth",
				// AuthStyle: oauth2.AuthStyleInHeader,
			},
			RedirectURL: redirectURL,
			Scopes:      []string{"openid", "offline", "offline_access"},
		},
handler for
/login
:
Copy code
func (h *Handlers) HandleLogin(w http.ResponseWriter, r *http.Request) {
	
	// h.PerformLogin()
	loginChal := r.URL.Query().Get("login_challenge")
	flowID := r.URL.Query().Get("flow")
	
	// redirect to login if there is no login challenge or flow  
	if loginChal == "" && flowID == "" {
		log.Println("No login challenge found or flow ID")

		st := make([]byte, 32)
		_, err := rand.Read(st)
		if err != nil {
			logrus.Error("generate state failed: %v", err)
			return
		}
		state := base64.StdEncoding.EncodeToString(st)
		setSessionValue(w, r, "oauth2State", state)

		// start oauth2 authorization code flow
		redirectTo := h.OAuth2Config.AuthCodeURL(state)
		logrus.Infof("redirect to hydra, url: %s", redirectTo)
		http.Redirect(w, r, redirectTo, http.StatusFound)
		return
	}

	var metadata Metadata

	// get login request from hydra only if there is no flow id in the url query parameters
	if flowID == "" {
		loginRes, _, err := h.HydraCl.HydraAPIClient.AdminApi.GetLoginRequest(r.Context()).LoginChallenge(loginChal).Execute()
		if err != nil {
			logrus.Error(err)
			writeError(w, http.StatusUnauthorized, errors.New("Unauthorized OAuth Client"))
			return
		}
		log.Println("got client id: ", loginRes.Client.ClientId)
		// client details from hydra
		clientRes, _, err := h.HydraCl.HydraAPIClient.AdminApi.GetOAuth2Client(r.Context(), *loginRes.Client.ClientId).Execute()
		if err != nil {
			logrus.Error(err)
			writeError(w, http.StatusUnauthorized, errors.New("Unauthorized OAuth Client"))
			return
		}

		log.Println("got client metadata: ", clientRes.Metadata)

		// convert map to json string
		md, err := json.Marshal(clientRes.Metadata)
		if err != nil {
			logrus.Error(err)
			writeError(w, http.StatusInternalServerError, errors.New("Unable to marshal metadata"))
			return
		}

		// convert json string to struct
		if err = json.Unmarshal([]byte(md), &metadata); err != nil {
			logrus.Error(err)
			writeError(w, http.StatusInternalServerError, errors.New("Internal Server Error"))
			return
		}
	}

	// store metadata value in session
	v := getSessionValue(w, r, "canRegister")
	reg, ok := v.(bool)
	if ok {
		metadata.Registration = reg
	} else {
		setSessionValue(w, r, "canRegister", metadata.Registration)
	}

	cookie := r.Header.Get("cookie")

	// check for kratos session details
	session, _, err := h.KratosCl.APIClient.V0alpha2Api.ToSession(r.Context()).Cookie(cookie).Execute()

	// if there is no session, redirect to login page with login challenge
	if err != nil {
		// build return_to url with hydra login challenge as url query parameter
		returnToParams := url.Values{
			"login_challenge": []string{loginChal},
		}
		returnTo := "/login?" + returnToParams.Encode()
		// build redirect url with return_to as url query parameter
		// refresh=true forces a new login from kratos regardless of browser sessions
		// this is important because we are letting Hydra handle sessions
		redirectToParam := url.Values{
			"return_to": []string{returnTo},
			"refresh":   []string{"true"},
		}
		redirectTo := fmt.Sprintf("%s/self-service/login/browser?", h.KratosCl.KratosPublicURL) + redirectToParam.Encode()

		// get flowID from url query parameters
		flowID := r.URL.Query().Get("flow")
		logrus.Infof("FLOW_ID: %s", flowID)

		// if there is no flow id in url query parameters, create a new flow
		if flowID == "" {
			http.Redirect(w, r, redirectTo, http.StatusFound)
			return
		}

		// get cookie from headers
		cookie := r.Header.Get("cookie")

		// get the login flow
		flow, _, err := h.KratosCl.APIClient.V0alpha2Api.GetSelfServiceLoginFlow(r.Context()).Id(flowID).Cookie(cookie).Execute()
		if err != nil {
			writeError(w, http.StatusUnauthorized, err)
			return
		}
		templateData := templateData{
			Title:    "Login",
			UI:       &flow.Ui,
			Metadata: metadata,
		}

		// render template index.html
		templateData.Render(w, "loginpassword.html")
		return
	}

	// if there is a valid session, marshal session.identity.traits to json to be stored in subject
	traitsJSON, err := json.Marshal(session.Identity.Traits)
	if err != nil {
		writeError(w, http.StatusInternalServerError, err)
		return
	}
	subject := string(traitsJSON)

	hydraReq := models.HydraLoginRequest{
		Remember:    false,
		RememberFor: 0,
		Subject:     subject,
	}
	// accept hydra login request
	redirectURL, err := h.HydraCl.AcceptLoginRequest(&hydraReq, loginChal)
	if err != nil {
		logrus.Error(err)
		writeError(w, http.StatusUnauthorized, errors.New("Unauthorized OAuth Client"))
		return
	}

	http.Redirect(w, r, redirectURL, http.StatusFound)

}
I can’t point you to a repo due to restrictions, I have provided necessary snippets, do tell if anything more is required.
Copy code
ERRO[0018] 401 Unauthorized                             
{
  "created_at": "2022-10-15T09:40:13.549878Z",
  "expires_at": "2022-10-15T09:50:13.544491Z",
  "id": "e201e674-732a-4102-a2c3-7f41c9d6ffc3",
  "issued_at": "2022-10-15T09:40:13.544491Z",
  "refresh": true,
  "request_url": "<http://127.0.0.1:9010/self-service/login/browser?refresh=true>\u0026return_to=%2Flogin%3Flogin_challenge%3D5339b74418e14dcd9a9bf27617887d1a",
  "requested_aal": "aal1",
  "return_to": "/login?login_challenge=5339b74418e14dcd9a9bf27617887d1a",
  "type": "browser",
  "ui": {
    "action": "<http://127.0.0.1:9010/self-service/login?flow=e201e674-732a-4102-a2c3-7f41c9d6ffc3>",
    "messages": [
      {
        "context": {},
        "id": 1010003,
        "text": "Please confirm this action by verifying that it is you.",
        "type": "info"
      }
    ],
    "method": "POST",
    "nodes": [
      {
        "attributes": {
          "disabled": false,
          "name": "provider",
          "node_type": "input",
          "type": "submit",
          "value": "github"
        },
        "group": "oidc",
        "messages": [],
        "meta": {
          "label": {
            "context": {
              "provider": "github"
            },
            "id": 1010002,
            "text": "Sign in with github",
            "type": "info"
          }
        },
        "type": "input"
      },
      {
        "attributes": {
          "disabled": false,
          "name": "csrf_token",
          "node_type": "input",
          "required": true,
          "type": "hidden",
          "value": "GkX+v5//hMAJ5CEj8FU0sqGqfLp26Glc88iz+jz5/vgsoGFTxxLBnpIxBkGnZKKCCRHa2Ym3VrpHDZvqhoS/zA=="
        },
        "group": "default",
        "messages": [],
        "meta": {},
        "type": "input"
      }
    ]
  },
  "updated_at": "2022-10-15T09:40:13.549878Z"
}
When I do
GetSelfServiceLoginFlow
above o/p I am getting, only the OIDC method works, password method is not working. // @bulky-architect-22083
130 Views