ccpgames / sso-issues

Please file issues with the CCP SSO (login.eveonline.com) here.
17 stars 1 forks source link

Help with identifying SSO native flow issues #80

Closed BigBallard closed 1 year ago

BigBallard commented 1 year ago

I am working on a native application that will utilize Eve SSO and ESI. I am writing this with Tauri (Rust). Currently I am stuck at the code verification phase when asking for an initial token.

Based from the developer instructions: To create a code challenge your application will first need to create a one time use code verifier. A simple way to do this is to generate 32 random bytes and base64url encode them. Store this code verifier as you’ll need it in a later step. To create a corresponding code challenge, SHA-256 hash the code verifier, and then base64url encode the raw hash output. The base64url encoding is defined in [RFC 4648](https://tools.ietf.org/html/rfc4648#section-5) and should not contain padding.

Here is where I generate the verifier and the code challenge

// generate 32 random bytes (64 in my case, shouldnt matter right?)
let mut rng = rand::thread_rng();
let bytes: Vec<u8> = (0..64).map(|_| {
    let i = rng.gen_range(0..CHARS.len());
    CHARS[i]
}).collect();

// base64url encode them
let random = base64::encode(from_utf8(bytes.as_slice()).unwrap().to_string());
println!("{random}");
// X2hoQ0IxSGhoTEZNQXV3Z2dsRHQwaHpuQXNadmI2MmpWampNNmJxVHlKTEN6RlktVm4tb0ZxVHMyZm1JcTQweQ==

// SHA-256 hash the code verifier
let mut hasher = Sha256::new();
hasher.update(random.clone());
let hash_res = format!("{:?}", hasher.finalize());

// base64url encode the raw hash output
let challenge = base64::encode(hash_res.as_bytes()).replace("=", "");
println!("{challenge}");
// WzIzNywgNSwgMzQsIDE5NywgOTksIDIxMSwgMTM0LCAyMCwgMTk1LCAxOTMsIDIzMywgMTM3LCAyMjQsIDUsIDEyNCwgMjMxLCAxNzEsIDIzOCwgMTYzLCAxNDEsIDE4NiwgMTUzLCAxNDYsIDQ5LCAxMjQsIDIwMCwgNTgsIDIyOSwgNDQsIDE5NywgMTkyLCAyMzZd

Both results look pretty reasonable to me with no major issues. Creating the URL for the login is successful at this point so now it is down to the POST to verify and get a token.

With the following instruction point: Now that your application has the authorization code, it needs to send a POST request to https://login.eveonline.com/v2/oauth/token with a payload containing the returned authorization code, the client ID of your application, and the original URL safe Base 64 encoded 32 byte string that was randomly created for the code challenge in step 3.

// Form values
 let form = [
    ("grant_type", "authorization_code"),
    ("client_id", "<my_id_value>"), // the client ID of your application
    ("code", code.as_str()), // returned authorization code
    ("code_verifier", verifier.as_str()) // original URL safe Base 64 encoded 32 byte string that was randomly.... (above value)
];

let mut client = reqwest::blocking::Client::new();
let response = client.post("https://login.eveonline.com/v2/oauth/token")
    .form(&form)
    .header("Content-Type", "application/x-www-form-urlencoded")
    .header("Host", "login.eveonline.com")
    .send()
    .unwrap();

The response I always get is a 400 with the following message: {"error":"invalid_grant","error_description":"Failed code verification challenge."}

At any point is my logic wrong?

CarbonAlabel commented 1 year ago

It looks like you are using the standard base64 encoding instead of base64url, and aren't stripping the padding characters. Using base64url encoding without padding ensures the code verifier contains only legal characters as defined in https://www.rfc-editor.org/rfc/rfc7636#section-4.1.

BigBallard commented 1 year ago

So I changed base64_url crate/library and modified the two lines:

let random = base64_url::encode(bytes.as_slice()).replace("=","");
...
let challenge = base64_url::encode(hash_res.as_bytes()).replace("=", "");

With results of TnZ4NVllcjdvTkpqNG5EdmVPX3Rzb2xvM0pZNTVLM1Iyc2J6Y1RMZDNYQm9uaFMuV0ZuNWU5aXdFNi43aDlFaA WzIwNiwgNzcsIDE5NCwgMjE3LCAxMjIsIDE3MywgOTUsIDI0OCwgMTMsIDE3NSwgNjAsIDE4NCwgMTE2LCAyMTIsIDEzLCA3MiwgOTIsIDE0OCwgMTYsIDk1LCAxMzMsIDIwLCAyLCAxNzgsIDIwNiwgNjYsIDgsIDEzOCwgMjM2LCAxNTIsIDE0NiwgNDdd respectively. Unfortunately I get the same error as above.

BigBallard commented 1 year ago

So I ran the python example and hardcoded a random value that I generated and printed out all the values.

Python results:

Random bytes: 4v8XbYu08g3jcZHIxwgHH7Y.xyqj6Do (hardcoded)
B64u random: NHY4WGJZdTA4ZzNqY1pISXh3Z0hIN1kueHlxajZEbw==
Hash: 6cf21572441026671e9668abac6bd4d778969adf9b8b2fb90b74bc8b095920d5
--> B64u hash: bPIVckQQJmcelmirrGvU13iWmt-biy-5C3S8iwlZINU

Rust results

Random bytes: 4v8XbYu08g3jcZHIxwgHH7Y.xyqj6Do
B64U random: NHY4WGJZdTA4ZzNqY1pISXh3Z0hIN1kueHlxajZEbw==
Hash: 6cf21572441026671e9668abac6bd4d778969adf9b8b2fb90b74bc8b095920d5
--> B64U hash: NmNmMjE1NzI0NDEwMjY2NzFlOTY2OGFiYWM2YmQ0ZDc3ODk2OWFkZjliOGIyZmI5MGI3NGJjOGIwOTU5MjBkNQ

As you can see, the base64 url encoding of the hash generates different values. I am using different library now for the B64 because the other was not padding as it should.

let byte_str: String = from_utf8(bytes.as_slice()).unwrap(); // Random bytes
let random: String = BASE64URL.encode(bytes.as_slice()); // B64U random
let hash_res: String = digest(random.clone()); // Hash
let challenge: String = BASE64URL.encode(hash_res.as_bytes()).replace("=", ""); // B64U hash.

The SHA256 lib is now sha256, and the base64 url encode is data_encoding

When I use an online B64U encoder I get the rust result instead of the python one so what could possibly be going on here?

CarbonAlabel commented 1 year ago

Might be the result of base64url encoding the hexadecimal representation of the hash, instead of the hash bytes themselves.

BigBallard commented 1 year ago

Yup, that ended up being it! What a headache...

I will probably submit a working example once I get this cleaned up.