Giter Site home page Giter Site logo

yew-oauth2's Introduction

OAuth2 (and OIDC) component for Yew

crates.io docs.rs CI

Add to your Cargo.toml:

yew-oauth2 = "0.11"

By default, the yew-nested-router integration for yew-nested-router is disabled. You can enable it using:

yew-oauth2 = { version = "0.10", features = ["yew-nested-router"] }

OpenID Connect

OpenID Connect requires an additional dependency and can be enabled using the feature openid.

Examples

A quick example of how to use it (see below for more complete examples):

use yew::prelude::*;
use yew_oauth2::prelude::*;
use yew_oauth2::oauth2::*; // use `openid::*` when using OpenID connect

#[function_component(MyApplication)]
fn my_app() -> Html {
    let config = Config::new(
        "my-client",
        "https://my-sso/auth/realms/my-realm/protocol/openid-connect/auth",
        "https://my-sso/auth/realms/my-realm/protocol/openid-connect/token"
    );

    html!(
    <OAuth2 {config}>
      <MyApplicationMain/>
    </OAuth2>
  )
}

#[function_component(MyApplicationMain)]
fn my_app_main() -> Html {
    let agent = use_auth_agent().expect("Must be nested inside an OAuth2 component");

    let login = use_callback(agent.clone(), |_, agent| {
        let _ = agent.start_login();
    });
    let logout = use_callback(agent, |_, agent| {
        let _ = agent.logout();
    });

    html!(
    <>
      <Failure><FailureMessage/></Failure>
      <Authenticated>
        <button onclick={logout}>{ "Logout" }</button>
      </Authenticated>
      <NotAuthenticated>
        <button onclick={login}>{ "Login" }</button>
      </NotAuthenticated>
    </>
  )
}

This repository also has some complete examples:

yew-oauth2-example

A complete example, hiding everything behind a "login" page, and revealing the content once the user logged in.

Use with either OpenID Connect or OAuth2.

yew-oauth2-redirect-example

A complete example, showing the full menu structure, but redirecting the user automatically to the login server when required.

Use with either OpenID Connect or OAuth2.

Testing

Testing the example projects locally can be done using a local Keycloak instance and trunk.

Start the Keycloak instance using:

podman-compose -f develop/docker-compose.yaml up

Then start trunk with the local developer instance:

cd yew-oauth2-example # or yew-oauth2-redirect-example
trunk serve

And navigate your browser to http://localhost:8080.

NOTE: It is important to use http://localhost:8080 instead of e.g. http://127.0.0.1:8080, as Keycloak is configured by default to use http://localhost:* as a valid redirect URL when in dev-mode. Otherwise, you will get an "invalid redirect" error from Keycloak.

yew-oauth2's People

Contributors

alexandreroba avatar anssip avatar ctron avatar derknerd avatar kate-shine avatar mh84 avatar ronnybremer avatar tommy-asd avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

yew-oauth2's Issues

Notes on using examples

Feel free to close this, I could be wrong on these but when trying the examples I had a few issues, though I would document them for others incase its wasn't just me.

Issue 1

When using the keycloak docker image, the default realm master endpoint
was "account" instead of "frontend".

Fix: Change client_id in oauth::config to "account" in app.rs files.

Issue 2

Default redirect_url doesn't include localhost

http://localhost:8081/admin/master/console/#/master/clients/

Default username is admin, password is admin123456

Select the "account" client

Update "Valid redirect URIs" with your url, in my case I added:
http://localhost:8080/*
http://127.0.0.1:8080/*

Note: Yes the readme notes the localhost is enabled by default, but that wasn't true for me, its my first time using keycloak so I could be missing something.

Issue 3

Error was "login result: failed to exchange code: Request failed"

Chrome was error due to CORS.

Update "Web Origins", (also found on "account" realm settings) to "+".

Refresh of access token issue

I have encountered a weird issue I can't quite get my bearings on. Taking to an OpenID IDP which returns an ID token, access token and expires_in setting of 1h (3600 seconds), the console log shows correctly:

yew-oauth2-0.10.2/src/agent/mod.rs:300 Starting timeout for: 3569940ms

The original request was made at 5:12:43 pm UTC time. I left the browser sit like that and was expecting the refresh to occur shortly before the 1h mark.
However, at 6:59:59 am the next day the refresh happened, and of course failed, as the refresh token was long expired.

yew-oauth2-0.10.2/src/agent/mod.rs:508 Triggering refresh
Failed to load resource: the server responded with a status of 400 (Bad Request)
yew-oauth2-0.10.2/src/agent/mod.rs:281 update state: Failed("login result: failed to exchange refresh token: Failed to parse server response")

More interesting, yew seemed to have crashed because of the error:

panicked at .cargo/registry/src/index.crates.io-6f17d22bba15001f/yew-0.21.0/src/functional/mod.rs:235:49:
assertion `left == right` failed: Hooks are called conditionally.
  left: 6
 right: 1

Stack:

@http://test:8080/frontend-43380d544d4c8f5e.js:642:30
<?>.wasm-function[console_error_panic_hook::Error::new::h0d77e00d11b5a90d]@[wasm code]
<?>.wasm-function[console_error_panic_hook::hook_impl::h95e550010f8cc388]@[wasm code]
<?>.wasm-function[console_error_panic_hook::hook::h5cb0f9de521c2ee5]@[wasm code]
<?>.wasm-function[core::ops::function::Fn::call::h19e24f327623c604]@[wasm code]
<?>.wasm-function[std::panicking::rust_panic_with_hook::h1e6ac5d404b8e31b]@[wasm code]
<?>.wasm-function[std::panicking::begin_panic_handler::{{closure}}::h24b0f4622f2766a5]@[wasm code]
<?>.wasm-function[std::sys_common::backtrace::__rust_end_short_backtrace::h19f35d272c126e7c]@[wasm code]
<?>.wasm-function[rust_begin_unwind]@[wasm code]
<?>.wasm-function[core::panicking::panic_fmt::h87755523850ece9e]@[wasm code]
<?>.wasm-function[core::panicking::assert_failed_inner::h161125ff96c9e54a]@[wasm code]
<?>.wasm-function[core::panicking::assert_failed::h4cdcdc97ec396f94]@[wasm code]
<?>.wasm-function[yew::functional::HookContext::assert_hook_context::h004ce7156897b47d]@[wasm code]
<?>.wasm-function[yew::functional::FunctionComponent<T>::render::ha75eb7effad19b6a]@[wasm code]
<?>.wasm-function[<frontend::pages::events::EventList as yew::html::component::BaseComponent>::view::ha60fae9c2ca27d2d]@[wasm code]
<?>.wasm-function[<yew::html::component::lifecycle::CompStateInner<COMP> as yew::html::component::lifecycle::Stateful>::view::h02c2ae1a9aeac644]@[wasm code]
<?>.wasm-function[yew::html::component::lifecycle::ComponentState::render::h8c12f7ba3c02ebe3]@[wasm code]
<?>.wasm-function[<yew::html::component::lifecycle::RenderRunner as yew::scheduler::Runnable>::run::h1b11947cb99d6bfa]@[wasm code]
<?>.wasm-function[yew::scheduler::start_now::scheduler_loop::h9a06f85de98fb156]@[wasm code]
<?>.wasm-function[yew::scheduler::start_now::{{closure}}::h3ad67fcc2dd55041]@[wasm code]
<?>.wasm-function[std::thread::local::LocalKey<T>::try_with::h465cb38313a5333e]@[wasm code]
<?>.wasm-function[std::thread::local::LocalKey<T>::with::h3c1687923204ba3b]@[wasm code]
<?>.wasm-function[yew::scheduler::start_now::h6c0ea53ccddacb93]@[wasm code]
<?>.wasm-function[yew::scheduler::arch::start::{{closure}}::h7f86f6ea030bef9e]@[wasm code]
<?>.wasm-function[wasm_bindgen_futures::task::singlethread::Task::run::h1c7ef169a360c4c1]@[wasm code]
<?>.wasm-function[wasm_bindgen_futures::queue::QueueState::run_all::h0185135a14ce071b]@[wasm code]
<?>.wasm-function[wasm_bindgen_futures::queue::Queue::new::{{closure}}::h7856e231edb3f147]@[wasm code]
<?>.wasm-function[<dyn core::ops::function::FnMut<(A,)>+Output = R as wasm_bindgen::closure::WasmClosure>::describe::invoke::h0096ac979af67ca1]@[wasm code]

RuntimeError: Unreachable code should not be executed (evaluating 'wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0096ac979af67ca1(arg0, arg1, addHeapObject(arg2))')

This is a Safari browser session, I will do more testing in Chrome and Firefox to see, if there is any difference.

What might cause the interval to be fired so late?

Using Auth0 as IDP and manage on Client the redirection.

Hi @ctron , Thanks for replying to me.

Here is the issue I'm facing. I'm trying to use Auth0 as IDP and I need to configure on Auth0 the authorized returnUrls.
Auth0 does not allow to have "Generic" returnURLs. So we need to have a fixed set of defined returned Url.

But when using Yew-Oauth (Which is a great Thanks for that :p) the token lives in the app context.So on any refresh or reload of the page we loose this token and we are redirected to auth0 to get a new token. Which makes full sens. But we can be redirected from any page on the app which requires authentication such as for example https://localhost:3000/orders/xxxx/items/details...... xxx could be infinite... So no way to configure this on Auth0.

One way to tackle this could be to use on our app a defined url such as /callback?appReturnUrl=<<https://localhost:3000/orders/xxxx/items/details must be url encoded of course :p>> or a state parameter which will contains the url of the protected ressource and redirect the user to this value when /callback is called.

In order to do so we need to be able to "Inject" the returnUrl when redirecting to the authorization endpoint when accessing a protected ressource taking the current page url and add it as the appREturnURl or state.

My understanding is that we should use for this the LoginOptions parameters, so as a first step I used this code:

let login_options = LoginOptions::new()
        .with_redirect_url(Url::parse("http://localhost:3000/callback").unwrap());
    html!(
        <>
            <OAuth2 {config}
                scopes={vec!["openid".into(),"email".into(),"offline_access".into(),"api:call".into()]}
                audience={"http://localhost:8081/api"}
                options={login_options}>
                <Content/>
            </OAuth2>
        </>
    )

My understanding is that it is supposed to override the default returnUrl with what it is specified on the with_redirect_url. But it does not. When the user tries to access a protected ressources he still get as returnUrl the url of the protected ressource:

response_type: code
client_id: XXXXXXXXXXXX
state: eMhCkjz4-qTShr93Iw-qmw
code_challenge: lwo9FNEKznQ7xgho_ylMrxU_71zHzQoHlq907uQ83yY
code_challenge_method: S256
redirect_uri: http://localhost:3000/
scope: openid openid email offline_access api:call
audience: http://localhost:8081/api
nonce: B4qwU8ekDmymf9b6Mzongw

Once I have this working I will need a way on this configuration to inject the url of the protected ressource as the appReturnURL...

I do not see how to do this differently in order to implement the silent login on an unlimited custom list of return urls with auth0 as they do not support wildcard parameters on the return url :(

This is also what is explain here https://auth0.com/docs/authenticate/login/redirect-users-after-login.

Support yew 0.21

When upgrading the examples to yew 0.21 i discovered that it is not compatible, so i needed to make some changes i want to share.

#17 will fix the parts in this library.

Authentication State Doesn't Persist Through Page Refresh

First of all thank you for yew-oauth2 - having oauth in Rust webapps is amazing.

It seems like the access/refresh tokens are stored in an OAuth2Context object that can be retrieved by child components of <OAuth2>. Consequently, auth state seems to be lost when the page is refreshed.

I'm not an expert in auth flows, but wouldn't storing the access token in a cookie be preferable? Both to resolve this issue and because it's likely all or most requests to backend APIs will need to include the access token anyway.

I suppose that would make this crate a bit "opinionated" in that some developers might want to handle this a different way. What's your recommended approach for handling this given how the crate works today?

Supporting Google OAuth2

What is the recommended way to support Google's OAuth2 implementation. Google requires scope and has many additional parameters. As an example:

https://accounts.google.com/o/oauth2/v2/auth?
 scope=https%3A//www.googleapis.com/auth/drive.metadata.readonly&
 access_type=offline&
 include_granted_scopes=true&
 response_type=code&
 state=state_parameter_passthrough_value&
 redirect_uri=https%3A//oauth2.example.com/code&
 client_id=client_id

Provide a way to redirect when not logged in

Currently there is a Redirect component, which redirects when the session becomes unauthenticated.

Maybe the name is bad, but even I just thought it would work differently.

That functionality is still relevant, but there should also be some functionality, which either renders its content (children) or starts a new login if it would render its children.

An alternative solution would be, to add a new property to the Authenticated component, which would mean "login otherwise".

Add support for the Next (after 0.19) version of Yew

I really like this lib, makes things so much easier!

I'm not able to use this component with the latest version of Yew. I know it isn't released yet, but I was hoping to upgrade my app to that in preparation for its release.

The error I'm getting is:

34 | pub struct OAuth2<C: Client> {
   | ---------------------------- doesn't satisfy `_: yew::BaseComponent`
   |
   = note: the following trait bounds were not satisfied:
           `yew_oauth2::components::context::OAuth2<OAuth2Client>: yew::BaseComponent`

I understand if the Yew code base is too fluid at the moment, so happy if it sits here until it's closer to release. I might come back and try and do it myself.

Not configurable 'redirect_url' for a openidclient

I need to integrate with OKTA my yew application. For Swagger I used redirect_url as part of configuration with some other params.
I saw in code used some default value like http://localhost:8080/index.html. Okta is required http://localhost:8080/login/oauth2/code/okta.
I didn't find any way to configure it.
Please expose a way to configure that.

Router seems to work fine without the router feature

Initially I enabled the router feature, and specified 'yew-router-nested' as package for yew-router.

yew-router = { version = "0.16", package = "yew-router-nested" }

I noticed the router in the example code uses conventions from the Yew v0.18 documentation.

Just for fun I tried removing the "package" attribute and removing the yew-oauth2 router feature, and using the router macros and structs from the Yew v0.19 documentation. Everything seemed to continue working as expected (my component, which is below the BrowserRouter, is able to get the OAuth2Context context as expected).

What's the backstory on why the router feature is needed and any idea why things seem to work without it?

How to retrieve additional claims from the ID token?

I figured out how to get access to the claims by following the example here https://github.com/ctron/yew-oauth2/blob/17647840a9a951cee93db56a0cf0f0bb19696a79/yew-oauth2-example/src/components/identity.rs

However, is it really the only option to first serialize the claims to Json and then use Serde to serialize them back into a structure holding the claims? The additonal_claims() function only returns an EmptyAdditionalClaims struct, so I assume I am missing somewhere the option to specify my own structure during the client construction. Any help would be highly appreciated.

post_login_redirect doesn't actually redirect if the callback is not used

I was implementing the OpenID workflow in a new Yew app with this crate. Works like a charm until I need to be redirected after login. It removes the URL parameters as returned from authorize but doesn't redirect.

Looking at the following piece of code, when no callback is set the function just returns, without deleting the previous URL form local storage nor trying to redirect.

yew-oauth2/src/agent/mod.rs

Lines 448 to 456 in 1764784

fn post_login_redirect(&self) -> Result<(), OAuth2Error> {
let config = self.config.as_ref().ok_or(OAuth2Error::NotInitialized)?;
let Some(redirect_callback) = config
.default_login_options
.as_ref()
.and_then(|opts| opts.post_login_redirect_callback.clone())
else {
return Ok(());
};

The documentation earlier in the same file

/// the back to the original page, but only after the issuer redirected back the `redirect_url`. If, while starting the
/// login process, the currently active URL differs from the `redirect_url`, the agent will store the "current" URL and
/// redirect to it once the login process has completed.

states, that the redirect will be "done". I assume that is not the case and I need to implement the callback myself.

Is that correct?

OpenID Connect Client authentication

Quick question, I am looking for client authentication for OpenID, but did not find a field in the configuration additional field for OpenID. Is there something I am missing, or are non public clients not implemented nor planned.

Otherwise, great work.

redirect_uri_mismatch

Hi, this is just a question.
I have been trying to use google-auth. But I keep getting this error even though I have configured my oauth2 client's ID redirect_url correctly. I just used the example available in the docs and haven't modified much.

image

Is there anything I am missing?

use yew::prelude::*;
use yew_oauth2::prelude::*;
use yew_oauth2::oauth2::*;
// use gloo::console::log;


#[function_component(MyApplication)]
pub fn my_app() -> Html {
    // google login implementation
    let config = Config {
        client_id: "289520722885-3ur4cdpsj0a2phdjh05adrg3jeuvc8ke.apps.googleusercontent.com".into(),
        auth_url: "https://accounts.google.com/o/oauth2/auth".into(),
        token_url: "https://oauth2.googleapis.com/token".into(),
    };
    let scopes = vec![ "https://www.googleapis.com/auth/userinfo.profile".into(),];

    html!(
        <OAuth2 config={config} scopes={scopes}>
            <MyApplicationMain/>
        </OAuth2>
        )
}

#[function_component(MyApplicationMain)]
pub fn my_app_main() -> Html {
    let agent = use_auth_agent().expect("Must be nested inside an OAuth2 component");

    let login = {
        let agent = agent.clone();
        Callback::from(move |_| {
            let _ = agent.start_login();
        })
    };
    let logout = Callback::from(move |_| {
        let _ = agent.logout();
    });

    html!(
        <>
            <Failure><FailureMessage/></Failure>
            <Authenticated>
                <button onclick={logout}>{ "Logout" }</button>
            </Authenticated>
            <NotAuthenticated>
                <button onclick={login}>{ "Login" }</button>
            </NotAuthenticated>
        </>
        )
}

Store and Reuse AccessToken for Silent Login

Once logged in, if a user reloads the SPA either by refreshing the page or navigating via the address bar, the login context is "lost", and the user must trigger another login. At least for my Azure AD provider the subsequent login redirects immediately without the need for interaction.

I wonder if it would it be possible to store the users access_token and expires in SessionStorage? That way when the context is configured, it could first check those stored values and attempt a silent login flow to obtain a new access, refresh, and id token. I believe the Microsoft Graph Toolkit components function in this way to persist the login state across page reloads.

What are your thoughts?

Could `Config::token_url` be made optional?

I'm trying to build a web app that uses a Cloudflare Worker as my "serverless" server to support OAuth authentication with GitHub.

GitHub has you provide an "Authorization callback URL" when you register your app, which is (I'm pretty sure) the token_url here.

If you provide a (what GitHub calls a) redirect_uri in your initial login request that must match what you provided when you register your app, or you get an error. You can also leave it out, and then GitHub will just use the URL you provided when you registered the app.

I'm having problems that I think are because of some URL rewriting that Cloudflare is doing, and I think a reasonable way out of my predicament would be to just not provide a callback URL in the request and let GitHub use the one I registered with the app.

At the moment, though, Config requires a token_url, so I can't leave it out of the request if I use this (nice) library. Could we make the token_url an Option<String> type so that people could leave it out if they wanted/needed to?

I'm out at the edges of things I understand here (new-ish to Rust and Yew, limited experience with OAuth, no experience with Cloudflare Workers), so feel free to tell me I'm up a tree on all this. ๐Ÿ˜œ

Thanks!

Time not implemented on this platform on yew-oauth2-example

Hi,
First of all thanks for your amazing work here. :) .

I am learning bit about oauth2 and wanted to integrate this on my app, but running the yew-oauth2-example as per the readme, seems to log in successfully, but looking at the logs, I get an error regarding time is not implemented. Is it expected?

docker-compose -f develop/docker-compose.yaml up
cd yew-oauth2-example
trunk serve

And then get this:

panicked at 'time not implemented on this platform', library/std/src/sys/wasm/../unsupported/time.rs:31:9

Stack:

__wbg_get_imports/imports.wbg.__wbg_new_abda76e883ba8a5f@http://localhost:8080/yew-oauth2-example-967c702dc098d627.js:399:21
console_error_panic_hook::Error::new::he7fcaa1455222268@http://localhost:8080/yew-oauth2-example-967c702dc098d627_bg.wasm:wasm-function[16485]:0x797bef
console_error_panic_hook::hook_impl::hcb1643e4297f9336@http://localhost:8080/yew-oauth2-example-967c702dc098d627_bg.wasm:wasm-function[4020]:0x53c00b
console_error_panic_hook::hook::h14fd4bfad2bc6b1b@http://localhost:8080/yew-oauth2-example-967c702dc098d627_bg.wasm:wasm-function[22790]:0x7fe678
core::ops::function::Fn::call::he16fc9ba676c4956@http://localhost:8080/yew-oauth2-example-967c702dc098d627_bg.wasm:wasm-function[19593]:0x7d150a
std::panicking::rust_panic_with_hook::h9fe353d01bf1e5b0@http://localhost:8080/yew-oauth2-example-967c702dc098d627_bg.wasm:wasm-function[9018]:0x6a4101

And this is how the page looks, with empty <ul> elements:
image

Thank you :)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.