Skip to content

Commit 1151746

Browse files
committed
feat: allow providing an initial refresh token
1 parent 908a42d commit 1151746

File tree

3 files changed

+145
-80
lines changed

3 files changed

+145
-80
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,14 @@ Or even shorter:
6666
```bash
6767
http example.com/api $(oidc token -H my-client)
6868
```
69+
70+
## More examples
71+
72+
Create a public client from an initial refresh token. This can be useful if you have a frontend application, but no
73+
means
74+
of performing the authorization code flow with a local server. In case you have access to the refresh token, e.g via
75+
the browsers developer console, you can initialize the public client with that:
76+
77+
```bash
78+
oidc create public my-client --issuer https://example.com/realm --client-id foo --refresh-token <refresh-token>
79+
```

src/cmd/create/public.rs

Lines changed: 88 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ use crate::{
22
cmd::create::CreateCommon,
33
config::{Client, ClientType, Config},
44
http::{HttpOptions, create_client},
5-
oidc::extra_scopes,
5+
oidc::{extra_scopes, refresh_token_request},
66
server::{Bind, Server},
77
utils::OrNone,
88
};
99
use anyhow::{Context, bail};
1010
use oauth2::{
11-
AuthorizationCode, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, RedirectUrl,
12-
TokenResponse,
11+
AuthorizationCode, ClientId, ClientSecret, CsrfToken, EndpointMaybeSet, EndpointNotSet,
12+
EndpointSet, PkceCodeChallenge, RedirectUrl, TokenResponse,
1313
};
1414
use openidconnect::{
1515
AuthenticationFlow, IssuerUrl, Nonce,
16-
core::{CoreClient, CoreProviderMetadata, CoreResponseType},
16+
core::{CoreClient, CoreProviderMetadata, CoreResponseType, CoreTokenResponse},
1717
};
1818
use std::path::PathBuf;
1919

@@ -34,6 +34,10 @@ pub struct CreatePublic {
3434
#[arg(short = 's', long)]
3535
pub client_secret: Option<String>,
3636

37+
/// A refresh token to start with, instead of the authorization code flow
38+
#[arg(short = 'R', long)]
39+
pub refresh_token: Option<String>,
40+
3741
/// Force using a specific port for the local server
3842
#[arg(short, long)]
3943
pub port: Option<u16>,
@@ -58,6 +62,15 @@ pub struct CreatePublic {
5862
pub http: HttpOptions,
5963
}
6064

65+
type FlowClient = CoreClient<
66+
EndpointSet,
67+
EndpointNotSet,
68+
EndpointNotSet,
69+
EndpointNotSet,
70+
EndpointMaybeSet,
71+
EndpointMaybeSet,
72+
>;
73+
6174
impl CreatePublic {
6275
pub async fn run(self) -> anyhow::Result<()> {
6376
log::debug!("creating new client: {}", self.common.name);
@@ -71,9 +84,6 @@ impl CreatePublic {
7184
);
7285
}
7386

74-
let server = Server::new(self.bind_mode(), self.port).await?;
75-
let redirect = format!("http://localhost:{}", server.port);
76-
7787
let http = create_client(&self.http).await?;
7888

7989
let provider_metadata = CoreProviderMetadata::discover_async(
@@ -86,8 +96,75 @@ impl CreatePublic {
8696
provider_metadata,
8797
ClientId::new(self.client_id.clone()),
8898
self.client_secret.clone().map(ClientSecret::new),
89-
)
90-
.set_redirect_uri(RedirectUrl::new(redirect)?);
99+
);
100+
101+
let token = match self.refresh_token {
102+
None => self.code_flow(&http, &client).await?,
103+
Some(refresh_token) => {
104+
refresh_token_request(&http, &client, self.common.scope.as_deref(), refresh_token)
105+
.await?
106+
}
107+
};
108+
109+
// log info
110+
111+
log::info!("First token:");
112+
log::info!(
113+
" ID: {}",
114+
OrNone(
115+
&token
116+
.extra_fields()
117+
.id_token()
118+
.cloned()
119+
.map(|t| t.to_string())
120+
)
121+
);
122+
log::info!(" Access: {}", token.access_token().clone().into_secret());
123+
log::info!(
124+
" Refresh: {}",
125+
OrNone(&token.refresh_token().cloned().map(|t| t.into_secret()))
126+
);
127+
128+
// create client
129+
130+
let client = Client {
131+
issuer_url: self.common.issuer,
132+
scope: self.common.scope,
133+
r#type: ClientType::Public {
134+
client_id: self.client_id,
135+
client_secret: self.client_secret,
136+
},
137+
state: Some(token.into()),
138+
};
139+
140+
config
141+
.clients
142+
.insert(self.common.name.clone(), client.clone());
143+
144+
config.store(self.config.as_deref())?;
145+
146+
Ok(())
147+
}
148+
149+
fn bind_mode(&self) -> Bind {
150+
if self.only4 {
151+
Bind::Only4
152+
} else if self.only6 {
153+
Bind::Only6
154+
} else {
155+
self.bind
156+
}
157+
}
158+
159+
async fn code_flow(
160+
&self,
161+
http: &reqwest::Client,
162+
client: &FlowClient,
163+
) -> anyhow::Result<CoreTokenResponse> {
164+
let server = Server::new(self.bind_mode(), self.port).await?;
165+
let redirect = format!("http://localhost:{}", server.port);
166+
167+
let client = client.clone().set_redirect_uri(RedirectUrl::new(redirect)?);
91168

92169
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
93170

@@ -138,7 +215,7 @@ Open the following URL in your browser and perform the interactive login process
138215
let token = client
139216
.exchange_code(AuthorizationCode::new(result.code))?
140217
.set_pkce_verifier(pkce_verifier)
141-
.request_async(&http)
218+
.request_async(http)
142219
.await?;
143220

144221
// check ID token
@@ -150,53 +227,6 @@ Open the following URL in your browser and perform the interactive login process
150227
.context("failed to verify ID token")?;
151228
}
152229

153-
// log info
154-
155-
log::info!("First token:");
156-
log::info!(
157-
" ID: {}",
158-
OrNone(
159-
&token
160-
.extra_fields()
161-
.id_token()
162-
.cloned()
163-
.map(|t| t.to_string())
164-
)
165-
);
166-
log::info!(" Access: {}", token.access_token().clone().into_secret());
167-
log::info!(
168-
" Refresh: {}",
169-
OrNone(&token.refresh_token().cloned().map(|t| t.into_secret()))
170-
);
171-
172-
// create client
173-
174-
let client = Client {
175-
issuer_url: self.common.issuer,
176-
scope: self.common.scope,
177-
r#type: ClientType::Public {
178-
client_id: self.client_id,
179-
client_secret: self.client_secret,
180-
},
181-
state: Some(token.into()),
182-
};
183-
184-
config
185-
.clients
186-
.insert(self.common.name.clone(), client.clone());
187-
188-
config.store(self.config.as_deref())?;
189-
190-
Ok(())
191-
}
192-
193-
fn bind_mode(&self) -> Bind {
194-
if self.only4 {
195-
Bind::Only4
196-
} else if self.only6 {
197-
Bind::Only6
198-
} else {
199-
self.bind
200-
}
230+
Ok(token)
201231
}
202232
}

src/oidc.rs

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ use crate::{
66
};
77
use anyhow::{anyhow, bail};
88
use biscuit::{Empty, jws::Compact};
9-
use oauth2::RefreshToken;
9+
use oauth2::{EndpointMaybeSet, EndpointNotSet, EndpointSet, RefreshToken};
1010
use openidconnect::{
1111
ClientId, ClientSecret, IssuerUrl, Scope,
12-
core::{CoreClient, CoreProviderMetadata},
12+
core::{CoreClient, CoreProviderMetadata, CoreTokenResponse},
1313
};
1414
use time::OffsetDateTime;
1515

@@ -67,32 +67,15 @@ pub async fn fetch_token(config: &Client, http: &HttpOptions) -> anyhow::Result<
6767

6868
let refresh_token = state.refresh_token.clone().ok_or_else(|| anyhow!("Expired token of a public client, without having a refresh token. You will need to re-login."))?;
6969

70-
if let Ok(token) = Compact::<RefreshTokenClaims, Empty>::new_encoded(&refresh_token)
71-
.unverified_payload()
72-
{
73-
log::debug!("refresh token expiration: {:?}", token.exp);
74-
75-
if let Some(exp) = token
76-
.exp
77-
.and_then(|exp| OffsetDateTime::from_unix_timestamp(exp).ok())
78-
{
79-
if exp < OffsetDateTime::now_utc() {
80-
bail!("Refresh token expired. You need to re-login.");
81-
}
82-
}
83-
}
84-
8570
let client = CoreClient::from_provider_metadata(
8671
provider_metadata,
8772
ClientId::new(client_id.clone()),
8873
client_secret.clone().map(ClientSecret::new),
8974
);
9075

91-
let token = client
92-
.exchange_refresh_token(&RefreshToken::new(refresh_token))?
93-
.add_scopes(extra_scopes(config.scope.as_deref()))
94-
.request_async(&http)
95-
.await?;
76+
let token =
77+
refresh_token_request(&http, &client, config.scope.as_deref(), refresh_token)
78+
.await?;
9679

9780
Ok(TokenResult::Refreshed(token.into()))
9881
}
@@ -119,3 +102,44 @@ pub fn extra_scopes(scope: Option<&str>) -> impl Iterator<Item = Scope> {
119102
.flat_map(|s| s.split(' '))
120103
.map(|s| Scope::new(s.into()))
121104
}
105+
106+
pub fn check_refresh_token_expiration(refresh_token: &str) -> anyhow::Result<()> {
107+
if let Ok(token) =
108+
Compact::<RefreshTokenClaims, Empty>::new_encoded(refresh_token).unverified_payload()
109+
{
110+
log::debug!("refresh token expiration: {:?}", token.exp);
111+
112+
if let Some(exp) = token
113+
.exp
114+
.and_then(|exp| OffsetDateTime::from_unix_timestamp(exp).ok())
115+
{
116+
if exp < OffsetDateTime::now_utc() {
117+
bail!("Refresh token expired. You need to re-login.");
118+
}
119+
}
120+
}
121+
122+
Ok(())
123+
}
124+
125+
pub async fn refresh_token_request(
126+
http: &reqwest::Client,
127+
client: &CoreClient<
128+
EndpointSet,
129+
EndpointNotSet,
130+
EndpointNotSet,
131+
EndpointNotSet,
132+
EndpointMaybeSet,
133+
EndpointMaybeSet,
134+
>,
135+
scope: Option<&str>,
136+
refresh_token: String,
137+
) -> anyhow::Result<CoreTokenResponse> {
138+
check_refresh_token_expiration(&refresh_token)?;
139+
140+
Ok(client
141+
.exchange_refresh_token(&RefreshToken::new(refresh_token))?
142+
.add_scopes(extra_scopes(scope))
143+
.request_async(http)
144+
.await?)
145+
}

0 commit comments

Comments
 (0)