Comments (12)
I have a ton of octocrab changes queued up. I wanted to get my app working before I submitted them because it's really hard to test them in any other way :-(. They're here: https://github.com/Pernosco/octocrab/tree/events.
I'll rebase them and submit them.
from octocrab.
@aslamplr Thanks for sharing this, it's not directly applicable since Octocrab needs to work in non CLI projects such as servers, so things like opening the web browser wouldn't be possible, but it's a good place to start.
from octocrab.
@dkowis thanks you saved me some time.
In case this saves anyone else some time... here's a very quick very dirty example of authenticating as an installation built on top of dkowis' JWT snippet.
#[cfg(test)]
mod tests {
use super::*;
use crate::github::app_authentication::get_jwt;
#[derive(Debug, Deserialize)]
struct Installation {
access_tokens_url: String,
}
#[derive(Debug, Deserialize)]
struct Access {
token: String,
}
#[derive(Debug, Serialize)]
struct CreateAccessToken {
repositories: Vec<String>,
}
#[actix_rt::test]
async fn foo() {
let jwt = get_jwt(APP_IDENTIFIER.to_string(), &PRIVATE_KEY).unwrap();
let octo = octocrab::OctocrabBuilder::new()
.personal_token(jwt)
.build()
.unwrap();
let installations: Vec<Installation> =
octo.get("/app/installations", None::<&()>).await.unwrap();
let access: Access = octo
.post(
installations[0].access_tokens_url.as_str(),
Some(&CreateAccessToken {
repositories: vec!["repo-name".to_string()],
}),
)
.await
.unwrap();
let octo = octocrab::OctocrabBuilder::new()
.personal_token(access.token)
.build()
.unwrap();
let comment = octo
.issues("account-name", "repo-name")
.create_comment(121, "Created programatically!")
.await
.unwrap();
dbg!(comment);
}
}
from octocrab.
Hi 👋 I have a working implementation in Rust roughly based on the official GitHub’s cli project in Golang. It’s not refined or idiomatic Rust but works. https://github.com/aslamplr/gh-cli/blob/2dc15b56a3f233a5ae7f6fa33664fa995e072cbe/gh-auth/src/lib.rs#L6
from octocrab.
I have done a small bit of this, using a github app. Pasting it here for reference for others.
I used the oauth2 crate and jsonwebtoken crate. The client credentials grant allows my server side app to use the client ID/Secret, and the private key provided, to retrieve a token that can call github apis. This works if you plop it into the personal_token
github call.
use jsonwebtoken::{EncodingKey, Algorithm, Header, encode};
use std::fs::File;
use std::io::prelude::*;
use serde::{Deserialize, Serialize};
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use thiserror::Error;
use crate::lfr::github_auth::AuthError::{NoPrivateKeyFound, TokenError};
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
iss: String,
iat: usize,
exp: usize,
}
#[derive(Error, Debug)]
pub enum AuthError {
#[error("Unable to read private key")]
NoPrivateKeyFound(#[from] std::io::Error),
#[error("Unable to generate token")]
TokenError(#[from] jsonwebtoken::errors::Error)
}
pub fn get_jwt(priv_key_path: &str) -> Result<String, AuthError> {
let mut f = File::open(priv_key_path).map_err(NoPrivateKeyFound)?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;
let key = EncodingKey::from_rsa_pem(buffer.as_slice())?;
//Github needs an RS256 token
//10 minute maximum token lifetime
//issuer: 000000
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time Went Backwards...")
.as_secs() as usize;
let claims = Claims {
iat: now,
exp: now + (10 * 60),
iss: "000000".to_string() //github app id
};
let header = Header::new(Algorithm::RS256);
encode(&header, &claims, &key).map_err(TokenError)
}
This code is just how to get the app jwt which you can then use to get an installation token and use in a different octocrab client to make different API calls on the REPOs.
from octocrab.
I'm planning to implement this for Github Apps.
My API proposal is to add to Auth
like so:
pub enum Auth {
None,
PersonalToken(String),
App {
app_id: String,
key: jsonwebtoken::EncodingKey,
},
}
with a new method on OctocrabBuilder
, app(...)
. I also propose to add Octocrab::installation(InstallationId) -> Octocrab
which returns a new client for a specific installation. This supports the usual workflow of "authenticate as Github App; look up an installation id using /repos/{owner}/{repo}/installation
or something similar; then transition to working on that installation"
In the implementation, I would cache in Octocrab
a String authentication token for the app, and a String authentication token for the installation, if applicable, renewing those tokens automatically as required. I'd use jsonwebtoken
for JWT.
Does that sound good?
from octocrab.
with a new method on OctocrabBuilder, app(...). I also propose to add Octocrab::installation(InstallationId) -> Octocrab which returns a new client for a specific installation.
Hmm, I'm not sure on Octocrab::installation
, admittedly I do not use GitHub from an installation, so if this useful for you let me know, but doesn't it not make sense to have it just accept InstallationId
? Since that would require the installation being public, and I always assumed installations were private, and would also require auth.
In the implementation, I would cache in Octocrab a String authentication token for the app, and a String authentication token for the installation, if applicable, renewing those tokens automatically as required. I'd use jsonwebtoken for JWT.
How are we exposing the authentication workflow to the user? (re: the user of the library, not the GitHub user), I would want the solution to be compatible with clients which have no direct user input. So it needs to be programmatically accessible in some way.
from octocrab.
Hmm, I'm not sure on
Octocrab::installation
, admittedly I do not use GitHub from an installation, so if this useful for you let me know, but doesn't it not make sense to have it just acceptInstallationId
? Since that would require the installation being public, and I always assumed installations were private, and would also require auth.
For our app, authentication needs to use the following steps:
- We get a Github event containing repository info (username/reponame)
- We have to look up the installation id using https://docs.github.com/en/rest/reference/apps#get-a-repository-installation-for-the-authenticated-app. This request must be authenticated via our app, so we create a JWT token using our app ID and secret key, and send it in the
Bearer
header. - Then we need to create an authentication token for that installation, using https://docs.github.com/en/rest/reference/apps#create-an-installation-access-token-for-an-app. This also requires authentication via our app so we create another JWT token for that (or reuse the previous one, but it's no trouble to create a fresh token each time here since it's just client-side computation).
- Then we send various requests for that installation, authenticated using the previously acquired token, cached somewhere. Importantly, the installation token could expire (it's only valid for an hour), so we may need to repeat the previous step from time to time.
With the API I suggested this is easy to do from the library client's point of view:
- Create an
Octocrab
passing the app ID and JWT secret inAuth::App
- Send the "get repository installation" request using that
Octocrab
and extract the installation ID - Call
Octocrab::installation(id)
using that ID - Then using the new
Octocrab
, do the real work. At the first client API call it will create an installation token and cache it on theOctocrab
. Whenever the token expires it will need to create a new one.
This puts responsibility for refreshing the installation token onto Octocrab. It puts responsibility for getting the installation ID onto the client, but that's inevitable because there are several different ways you might want to get that ID. It makes it seamless for clients to transition from the "look up the installation ID" phase to the "working with that installation" phase, passing Octocrab state along if necessary (e.g. the cached app JWT token if we wanted to cache that).
An obvious alternative design would be to drop Octocrab::installation(id)
and instead have a 4th kind of Auth
, e.g. Auth::Installation { app_id, secret_key, installation_id }
and just create a fresh Octocrab
after we've determined the installation ID. I'm fine with that, it's just a little more verbose from the client's point of view and makes it a little bit harder to reuse existing Octocrab
state (if there is any).
from octocrab.
Ah okay, so if I so understand right the full signature is fn installation(self, installation_id: InstallationId) -> Self
? In which case that seems fine to me, I only think it should be named something like into_installation_client
. I think that more clearly communicates the intent.
from octocrab.
It doesn't consume self
(the same app server might need to spawn multiple clients to work with different installations), so probably installation_client
is better.
from octocrab.
Ah, true. That sounds good to me.
from octocrab.
@chinedufn Thank you for the snippet! I don’t know if don’t know if @rocallahan still intends to make a PR, but if not would you be up for making a PR adding those methods to the semantic API, and a authenticate_as_installation
method that accepts a jwt and does what’s included in your snippet? Then everyone can use it in the library directly.
from octocrab.
Related Issues (20)
- Listed example for github app auth doesn't work but another provided method does HOT 2
- Add generate-notes URL handler
- compilation failure with `default-features = false` HOT 1
- Cannot directly pass SecretString for token auth HOT 2
- Expose status_code in GithubError HOT 3
- Reference::Commit is based on a documentation error
- Authentication breaks for "child" clients HOT 1
- Should `RateLimit::reset` be a `u64`? HOT 1
- create_workflow_dispatch failed with GitHubError HOT 2
- Model bug in DiffEntry: Submodules have `"blob_url": null` and `"raw_url": null`
- Superseded by #625
- [pulls] Pull request review create/edit API HOT 1
- list_collaborators with permission
- Support remove_assignees on issue API
- Creating pr request with "head_repo" parameter
- field 'author' and 'committer' of type 'RepoCommitPage' are wrong HOT 1
- Support procuring a user access token from an oauth code HOT 1
- no method to create a repository hook
- Return result instead of panic when returning an `installation` Octocrab instance HOT 1
- Builder add_retry_config requires intermediate variable binding
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from octocrab.