Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OAuth2 authorization code flow example with CustomProtocol #127

Closed
1 of 2 tasks
jocutajar opened this issue Mar 24, 2021 · 6 comments
Closed
1 of 2 tasks

OAuth2 authorization code flow example with CustomProtocol #127

jocutajar opened this issue Mar 24, 2021 · 6 comments

Comments

@jocutajar
Copy link

jocutajar commented Mar 24, 2021

I've tried to complete an OAuth2 authorization code flow with wry and it worked! It demonstrates the CustomProtocol for the redirect URI with a custom scheme as requested in #39 . This is great because the alternative is either a device code flow, which is clumsy, or starting your own web server just to receive the redirect URL information...

Describe the solution you'd like
I'd like the example to be added, perhaps a little more polished.

Describe alternatives you've considered

  • Publishing a crate with this functionality. It is not something I'd like to commit to right now.
  • Another great feature would be to intercept web requests and perhaps rewrite them or serve them from the back-end. Any redirect-uri could be used then.

Would you assign yourself to implement this feature?

  • Yes, if welcome
  • No

Additional context
The tool opens a full screen authentication dialogue. One has to provide valid your-tenant-id, your-client-id, redirect-uri, ... After successful authentication or on failure, the tool receives the redirect URL and prints it to the console.

/*!

Example:

  oauthorize -a https://login.microsoftonline.com/your-tenant-id/oauth2/v2.0/authorize -c your-client-id -r urn:authenticate -s email -s profile -s openid

*/

use async_std::task::block_on;
use http_types::{Body, Url};
use log::*;
use serde::{Deserialize, Serialize};
use std::{
    sync::mpsc::{channel, Sender},
    time::Duration,
};
use structopt::StructOpt;
use wry::{Application, Attributes, CustomProtocol, RpcRequest, RpcResponse, WindowProxy};

fn main() -> anyhow::Result<()> {
    env_logger::init();
    let opts = Opts::from_args();

    let (redirect_sender, redirect_receiver) = channel();
    
    authenticate(
        opts.authorize_url,
        opts.client_id,
        opts.scopes.iter(),
        opts.redirect_uri,
        Some(redirect_sender),
    )?;
    let url = redirect_receiver.recv_timeout(Duration::from_secs(1))?;
    println!("{}", url);
    Ok(())
}

#[derive(StructOpt)]
struct Opts {
    #[structopt(long, short)]
    /// OAuth2 authorize URL, such as "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
    authorize_url: Url,

    #[structopt(long, short)]
    /// Application ID, usually a UUID string such as "5c9da3e4-3032-4906-a57b-10a675e39154"
    client_id: String,

    #[structopt(long = "scope", short, name = "scope")]
    /// Security scopes to request; consider "email", "openid", "profile"
    scopes: Vec<String>,

    #[structopt(long, short)]
    /// Where will the browser be redirected with the code or error result?
    /// Use a URN or a custom scheme such as 'mytool' in "mytool://authenticate/" to handle this internally.
    redirect_uri: Url,
}

fn authenticate(
    base_url: impl AsRef<str>,
    client_id: impl AsRef<str>,
    scopes: impl Iterator<Item = impl AsRef<str>>,
    redirect_uri: impl AsRef<str>,
    redirect_handler: Option<Sender<String>>,
) -> anyhow::Result<()> {
    let req = AuthorizationCodeRequest {
        client_id: client_id.as_ref().to_owned(),
        redirect_uri: redirect_uri.as_ref().to_owned(),
        response_mode: "query".to_owned(),
        response_type: "code".to_owned(),
        scope: scopes.fold(String::new(), |mut scopes, scope| {
            if !scopes.is_empty() {
                scopes.push_str(" ")
            }
            scopes.push_str(scope.as_ref());
            scopes
        }),
        state: None,
        prompt: None,
        login_hint: None,
        code_challenge: None,
        code_challenge_method: None,
    };

    let mut app = Application::new()?;
    let mut query = to_urlenc(&req)?;
    query.insert_str(0, "?");
    query.insert_str(0, base_url.as_ref());

    info!("OAuth query: {:?}", query);

    let attributes = Attributes {
        url: Some(query),
        title: String::from("Authenticate with OAuth authorization code flow"),
        always_on_top: true,
        fullscreen: true,
        ..Default::default()
    };

    let custom_protocol = if let Some(sender) = redirect_handler {
        let sender = sender.clone();
        match Url::parse(redirect_uri.as_ref()) {
            Err(e) => {
                info!(
                    "Redirect uri cannot be parsed => custom handler will not be used. {}",
                    e
                );
                None
            }
            Ok(u)
                if u.scheme().is_empty()
                    || ["http", "https"].contains(&u.scheme().to_lowercase().as_str()) =>
            {
                info!(
                    "Redirect uri has {:?} scheme => custom handler will not be used.",
                    u.scheme()
                );
                None
            }
            Ok(u) => Some(CustomProtocol {
                name: u.scheme().to_owned(),
                handler: Box::new(move |url| match sender.send(url.to_owned()) {
                    Ok(()) => Ok(
                        b"<html><body>OK!<script>window.rpc.notify('done')</script></body></html>"
                            .to_vec(),
                    ),
                    Err(e) => Err(wry::Error::SenderError(e)),
                }),
            }),
        }
    } else {
        None
    };

    let wrpc =
        Box::new(
            move |proxy: WindowProxy, request: RpcRequest| match request.method.as_str() {
                "send" => unimplemented!("send url {:?}", request.params),
                "done" => proxy
                    .close()
                    .ok()
                    .map(|_| RpcResponse::new_result(request.id, request.params)),
                other => unimplemented!("Unexpected method {:?}", other),
            },
        );

    app.add_window_with_configs(attributes, Some(wrpc), custom_protocol, None)?;
    app.run();
    Ok(())
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
struct AuthorizationCodeRequest {
    client_id: String,
    redirect_uri: String,
    response_type: String,
    response_mode: String,
    scope: String,
    state: Option<String>,
    prompt: Option<String>,
    code_challenge: Option<String>,
    code_challenge_method: Option<String>,
    login_hint: Option<String>,
}

fn to_urlenc<T>(form: &T) -> anyhow::Result<String>
where
    T: Serialize,
{
    errish(block_on(errish(Body::from_form(form))?.into_string()))
}
fn errish<T>(
    r: std::result::Result<T, http_types::Error>,
) -> std::result::Result<T, anyhow::Error> {
    r.map_err(|e| anyhow::anyhow!(e))
}
@wusyong
Copy link
Member

wusyong commented Mar 24, 2021

Feel free to open PR for adding any example :)

@nothingismagick
Copy link
Member

Well, maybe this would be even better as a tauri plugin... Seems quite useful.

@jocutajar
Copy link
Author

jocutajar commented Mar 24, 2021

I'm probably not going to work on a plug-in, but you have my permission to harvest what's ripe in the code.

I'm a little troubled with making the example useful. It needs an OAuth endpoint, it needs an app set up, it needs a user in the system... Would you think we can expect that much from a casual example explorer? Or could you host some sample setup to try this with? Azure AD or Google or GitHub? I suspect that otherwise it will be just interesting code to look at and move on.

@jocutajar
Copy link
Author

Curiously, I managed to get my example through a GitLab OAuth. The only thing is I hit the same bug as samdroid-apps/something-for-reddit#61 - the screen says "Redirection to URL with a scheme that is not HTTP(S)", but right-click and reload gets me through. This is a webkit trouble according to the bug report. Example:

oauthorize -a https://gitlab.com/oauth/authorize -c 3ee80cff1118649558d2201945118ff4cb68247be07ec8723ccb9e1b6ad86548 -r oauth2://authenticate/ -s email

I think something similar could be done with GitHub so there is a working example.

@wusyong
Copy link
Member

wusyong commented Mar 24, 2021

I think the workaround could be starting another window but I'm not sure how it goes either.
Anyway, my original expectation is just a pseudo example for custom protocol.
Maybe we could just load some assets to showcase what it could do.

@wusyong
Copy link
Member

wusyong commented Apr 21, 2021

Close this because we now have a custom protocol example

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants