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

Better examples needed #28

Open
frenchtoastbeer opened this issue Aug 4, 2020 · 7 comments
Open

Better examples needed #28

frenchtoastbeer opened this issue Aug 4, 2020 · 7 comments

Comments

@frenchtoastbeer
Copy link

I eventually was able to take the samples of how to use this (specifically the await example) and turn it into something at compiled. A few things were necessary in order to do that, one of which centered around error handling. I'm sure I did it wrong, but without a better option I'm just going to posit here what I did so that you'll know that A - it didn't appear to me that your example usage code was functional and B - maybe having my sample code helps get to a better solution faster.

cargo.toml

[dependencies]
openidconnect = { version = "1.0", features = ["futures-03","reqwest-010"], default-features = false }
oauth2 = "3.0"
failure = "0.1"

auth.rs

pub async fn authenticate() -> Result<(oauth2::AccessToken, Option<oauth2::RefreshToken>), MyErr> {
    use openidconnect::{
        AccessTokenHash,
        AsyncCodeTokenRequest,
        AuthorizationCode,
        ClientId,
        ClientSecret,
        CsrfToken,
        Nonce,
        IssuerUrl,
        PkceCodeChallenge,
        RedirectUrl,
        Scope,
    };
    use openidconnect::core::{
      CoreAuthenticationFlow,
      CoreClient,
      CoreProviderMetadata,
    };
    use openidconnect::reqwest::async_http_client;
    
    // Use OpenID Connect Discovery to fetch the provider metadata.
    use openidconnect::{OAuth2TokenResponse, TokenResponse};
    let provider_metadata = CoreProviderMetadata::discover_async(
        IssuerUrl::new("https://accounts.example.com".to_string())?,
        async_http_client,
    )
    .await?;
    
    // Create an OpenID Connect client by specifying the client ID, client secret, authorization URL
    // and token URL.
    let client =
        CoreClient::from_provider_metadata(
            provider_metadata,
            ClientId::new("client_id".to_string()),
            Some(ClientSecret::new("client_secret".to_string())),
        )
        // Set the URL the user will be redirected to after the authorization process.
        .set_redirect_uri(RedirectUrl::new("http://redirect".to_string())?);
    
    // Generate a PKCE challenge.
    let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
    
    // Generate the full authorization URL.
    let (auth_url, csrf_token, nonce) = client
        .authorize_url(
            CoreAuthenticationFlow::AuthorizationCode,
            CsrfToken::new_random,
            Nonce::new_random,
        )
        // Set the desired scopes.
        .add_scope(Scope::new("read".to_string()))
        .add_scope(Scope::new("write".to_string()))
        // Set the PKCE code challenge.
        .set_pkce_challenge(pkce_challenge)
        .url();
    
    // This is the URL you should redirect the user to, in order to trigger the authorization
    // process.
    println!("Browse to: {}", auth_url);
    
    // Once the user has been redirected to the redirect URL, you'll have access to the
    // authorization code. For security reasons, your code should verify that the `state`
    // parameter returned by the server matches `csrf_state`.
    
    // Now you can exchange it for an access token and ID token.
    let token_response =
        client
            .exchange_code(AuthorizationCode::new("some authorization code".to_string()))
            // Set the PKCE code verifier.
            .set_pkce_verifier(pkce_verifier)
            .request_async(async_http_client)
            .await?;
    
    // Extract the ID token claims after verifying its authenticity and nonce.
    let id_token = token_response
      .id_token()
      .ok_or_else(|| failure::format_err!("Server did not return an ID token"))?;
    let claims = id_token.claims(&client.id_token_verifier(), &nonce)?;
    
    // Verify the access token hash to ensure that the access token hasn't been substituted for
    // another user's.
    if let Some(expected_access_token_hash) = claims.access_token_hash() {
        let actual_access_token_hash = AccessTokenHash::from_token(
            token_response.access_token(),
            &id_token.signing_alg()?
        )?;
        if actual_access_token_hash != *expected_access_token_hash {
            return Err(MyErr::from(failure::Error::from_boxed_compat("Invalid access token".into())));
        }
    }
    
    // The authenticated user's identity is now available. See the IdTokenClaims struct for a
    // complete listing of the available claims.
    println!(
        "User {} with e-mail address {} has authenticated successfully",
        claims.subject().as_str(),
        claims.email().map(|email| email.as_str()).unwrap_or("<not provided>"),
    );
    
    // See the OAuth2TokenResponse trait for a listing of other available fields such as
    // access_token() and refresh_token().
    let access_token = token_response.access_token().clone();
    if let Some(refresh_token) = token_response.refresh_token() {
        Ok((access_token, Some(refresh_token.clone())))
    } else {
        Ok((access_token, None))
    }
}

#[derive(Debug)]
enum Errors {
    Discovery(openidconnect::DiscoveryError<oauth2::reqwest::Error<reqwest::Error>>),
    RequestToken(oauth2::RequestTokenError<oauth2::reqwest::Error<reqwest::Error>, oauth2::StandardErrorResponse<oauth2::basic::BasicErrorResponseType>>),
    Parse(url::ParseError),
    Signing(openidconnect::SigningError),
    Failure(failure::Error),
    ClaimsVerification(openidconnect::ClaimsVerificationError),
    None
}

impl Default for Errors {
    fn default() -> Errors {
        Errors::None
    }
}

#[derive(Debug, Default)]
struct MyErr {
    error: Errors
}

impl std::fmt::Display for MyErr {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self.error {
            Errors::Discovery(e) => format!("{}",e),
            Errors::RequestToken(e) => format!("{}",e),
            Errors::Parse(e) => format!("{}",e),
            Errors::Signing(e) => format!("{}",e),
            Errors::Failure(e) => format!("{}",e),
            Errors::ClaimsVerification(e) => format!("{}",e),
            Errors::None => format!("Error wasn't captured."),
        }
    }
}
impl std::error::Error for MyErr {}

impl std::convert::From<openidconnect::DiscoveryError<oauth2::reqwest::Error<reqwest::Error>>> for MyErr {
    fn from(error: openidconnect::DiscoveryError<oauth2::reqwest::Error<reqwest::Error>>) -> MyErr {
        MyErr{
            error: Errors::Discovery(error)
        }
    }
}

impl std::convert::From<oauth2::RequestTokenError<oauth2::reqwest::Error<reqwest::Error>, oauth2::StandardErrorResponse<oauth2::basic::BasicErrorResponseType>>> for MyErr {
    fn from(error: oauth2::RequestTokenError<oauth2::reqwest::Error<reqwest::Error>, oauth2::StandardErrorResponse<oauth2::basic::BasicErrorResponseType>>) -> MyErr {
        MyErr{
            error: Errors::RequestToken(error)
        }
    }
}

impl std::convert::From<url::ParseError> for MyErr {
    fn from(error: url::ParseError) -> MyErr {
        MyErr{
            error: Errors::Parse(error)
        }
    }
}

impl std::convert::From<openidconnect::SigningError> for MyErr {
    fn from(error: openidconnect::SigningError) -> MyErr {
        MyErr{
            error: Errors::Signing(error)
        }
    }
}

impl std::convert::From<failure::Error> for MyErr {
    fn from(error: failure::Error) -> MyErr {
        MyErr{
            error: Errors::Failure(error)
        }
    }
}

impl std::convert::From<openidconnect::ClaimsVerificationError> for MyErr {
    fn from(error: openidconnect::ClaimsVerificationError) -> MyErr {
        MyErr{
            error: Errors::ClaimsVerification(error)
        }
    }
}
@zelda-at-tempus
Copy link

I myself would love to see examples of how to define your own additional claims. I've gotten so far as implementing the trait for a struct but I'm not sure what to do with it once I've done that.

@ramosbugs
Copy link
Owner

I'd definitely welcome PRs to address any gaps in the examples.

I myself would love to see examples of how to define your own additional claims. I've gotten so far as implementing the trait for a struct but I'm not sure what to do with it once I've done that.

The AC (additional claims) type parameter for the Client struct specifies the type of the additional claims. To receive custom claims, I recommend defining your own type alias similar to CoreClient but with the appropriate AC type. Then, you can access these claims in the ID token returned in the token response via IdTokenClaims::additional_claims.

To construct a new ID token with custom claims (i.e., when acting as an OIDC provider), the additional claims are one of the parameters to IdTokenClaims::new.

These unit tests might be helpful:

#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
struct TestClaims {
pub tfa_method: String,
}
impl AdditionalClaims for TestClaims {}
#[test]
fn test_additional_claims() {
let claims = serde_json::from_str::<IdTokenClaims<TestClaims, CoreGenderClaim>>(
"{
\"iss\": \"https://server.example.com\",
\"sub\": \"24400320\",
\"aud\": [\"s6BhdRkqt3\"],
\"exp\": 1311281970,
\"iat\": 1311280970,
\"tfa_method\": \"u2f\"
}",
)
.expect("failed to deserialize");
assert_eq!(claims.additional_claims().tfa_method, "u2f");
assert_eq!(
serde_json::to_string(&claims).expect("failed to serialize"),
"{\
\"iss\":\"https://server.example.com\",\
\"aud\":[\"s6BhdRkqt3\"],\
\"exp\":1311281970,\
\"iat\":1311280970,\
\"sub\":\"24400320\",\
\"tfa_method\":\"u2f\"\
}",
);
serde_json::from_str::<IdTokenClaims<TestClaims, CoreGenderClaim>>(
"{
\"iss\": \"https://server.example.com\",
\"sub\": \"24400320\",
\"aud\": [\"s6BhdRkqt3\"],
\"exp\": 1311281970,
\"iat\": 1311280970
}",
)
.expect_err("missing claim should fail to deserialize");
}
#[derive(Debug, Deserialize, Serialize)]
struct AllOtherClaims(HashMap<String, serde_json::Value>);
impl AdditionalClaims for AllOtherClaims {}
#[test]
fn test_catch_all_additional_claims() {
let claims = serde_json::from_str::<IdTokenClaims<AllOtherClaims, CoreGenderClaim>>(
"{
\"iss\": \"https://server.example.com\",
\"sub\": \"24400320\",
\"aud\": [\"s6BhdRkqt3\"],
\"exp\": 1311281970,
\"iat\": 1311280970,
\"tfa_method\": \"u2f\",
\"updated_at\": 1000
}",
)
.expect("failed to deserialize");
assert_eq!(claims.additional_claims().0.len(), 1);
assert_eq!(claims.additional_claims().0["tfa_method"], "u2f");
}

@zelda-at-tempus
Copy link

I feel so foolish that I didn't even think to check if you had unit tests. Thanks for pointing me in that direction

@benjaminSchilling33
Copy link
Contributor

benjaminSchilling33 commented Jan 29, 2021

Please ignore everything below, I finally found the issue, I will open up a PR to show how UserInfo can be requested in the example.

See: #36

@ramosbugs somehow related to the examples:

Can you enhance the example with a UserInfo Request?

I'm trying it like this:

let userinfo_request = match client.user_info(*token_response.access_token(), None) {
    Ok(o) => o,
    Err(e) => return Err(format!("No user info endpoint: {:?}", e)),
};
let userinfo = match userinfo_request.request(http_client) {
    Ok(o) => o,
    Err(e) => return Err(format!("Failed requesting user info: {:?}", e)),
};

But it complains about type annotations needed for openidconnect::UserInfoClaims<AC, GC>` in line 5 of the code above.

And when I explicitly add the type for userinfo like this:
: UserInfoClaims<MyClaim, GenderClaim> it complains about

the size for values of type `(dyn openidconnect::GenderClaim + 'static)` cannot be known at compilation time
doesn't have a size known at compile-time

Maybe I have to dig deeper, but I can't really figure out how to solve this.
Any hint is very appreciated.

@domma
Copy link

domma commented May 25, 2023

The AC (additional claims) type parameter for the Client struct specifies the type of the additional claims. To receive custom claims, I recommend defining your own type alias similar to CoreClient but with the appropriate AC type. Then, you can access these claims in the ID token returned in the token response via IdTokenClaims::additional_claims.

Could you elaborate a bit more on that? I gave this a try, but my Rust is not yet perfect. I tried to define a type alias of Client as you did for CoreClient. But that required me to also define a MyTokenResponse which required me to define a MyIdTokenFields`, ...

I did all of this, but at some point I could not call access_token() on the response anymore. Somehow it cannot get the implementation of TokenResponse and I'm totally lost now.

@ramosbugs
Copy link
Owner

The AC (additional claims) type parameter for the Client struct specifies the type of the additional claims. To receive custom claims, I recommend defining your own type alias similar to CoreClient but with the appropriate AC type. Then, you can access these claims in the ID token returned in the token response via IdTokenClaims::additional_claims.

Could you elaborate a bit more on that? I gave this a try, but my Rust is not yet perfect. I tried to define a type alias of Client as you did for CoreClient. But that required me to also define a MyTokenResponse which required me to define a MyIdTokenFields`, ...

I did all of this, but at some point I could not call access_token() on the response anymore. Somehow it cannot get the implementation of TokenResponse and I'm totally lost now.

Adding additional claims shouldn't require defining your own TokenResponse struct. Just use StandardTokenResponse<IdTokenFields<MyClaims, ...>, CoreTokenType>. The idea is to copy whichever typedefs from the core module need to be customized, and just modify the minimum set of type parameters.

@domma
Copy link

domma commented May 26, 2023

Adding additional claims shouldn't require defining your own TokenResponse struct. Just use StandardTokenResponse<IdTokenFields<MyClaims, ...>, CoreTokenType>. The idea is to copy whichever typedefs from the core module need to be customized, and just modify the minimum set of type parameters.

Thanks for your feedback. It got it working! The following code works for me to retrieve group information from Zitadel:

pub type ZitadelIdTokenFields = IdTokenFields<
    ZitadelClaims,
    EmptyExtraTokenFields,
    CoreGenderClaim,
    CoreJweContentEncryptionAlgorithm,
    CoreJwsSigningAlgorithm,
    CoreJsonWebKeyType,
>;

pub type ZitadelTokenResponse = StandardTokenResponse<ZitadelIdTokenFields, BasicTokenType>;


pub type Client = openidconnect::Client<
    ZitadelClaims,
    CoreAuthDisplay,
    CoreGenderClaim,
    CoreJweContentEncryptionAlgorithm,
    CoreJwsSigningAlgorithm,
    CoreJsonWebKeyType,
    CoreJsonWebKeyUse,
    CoreJsonWebKey,
    CoreAuthPrompt,
    StandardErrorResponse<CoreErrorResponseType>,
    ZitadelTokenResponse,
    BasicTokenType,
    CoreTokenIntrospectionResponse,
    CoreRevocableToken,
    CoreRevocationErrorResponse,
>;

#[derive(Serialize, Deserialize, Debug)]
pub struct ZitadelClaims {
    #[serde(alias = "urn:zitadel:iam:org:project:roles")]
    groups: HashMap<String, HashMap<String, String>>
}

impl AdditionalClaims for ZitadelClaims {}

One challenge for me was the existence of openidconnect::TokenResponse and oauth2::TokenResponse. Both need to be in scope for my client code and the error message if one is missing probably led me in a completely wrong direction. Now everything is working in a nice and simple way. Thanks again!

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

No branches or pull requests

5 participants