TiGa-RCE commited on
Commit
b4691b7
·
verified ·
1 Parent(s): e49e31a

Use standard HF PKCE redirect across OAuth opener isolation

Browse files
Files changed (5) hide show
  1. DATA_POLICY.md +7 -4
  2. RELEASE_MANIFEST.json +4 -5
  3. STORAGE_BOUNDARY.md +2 -1
  4. hf-auth.js +40 -36
  5. server.py +2 -5
DATA_POLICY.md CHANGED
@@ -35,11 +35,14 @@ of physical RAM/disk erasure, prevention of swap, or deletion of response buffer
35
  No claim is made about hosting-provider logs or infrastructure retention.
36
 
37
  Optional HF sign-in uses the official HF OAuth authorization-code/PKCE flow with
38
- only openid/profile scopes. Credentials and basic profile details live in browser
39
  page memory, not cookies, localStorage, recipes, or a server session database.
40
- HF handles password entry and authorization. The callback briefly carries a
41
- one-use authorization code and state in its URL; the app clears that URL before
42
- returning to the main window. HF/proxy infrastructure handling is outside this
 
 
 
43
  application's deletion guarantee. The app does not request email, billing,
44
  repository access, or persistent bucket access.
45
 
 
35
  No claim is made about hosting-provider logs or infrastructure retention.
36
 
37
  Optional HF sign-in uses the official HF OAuth authorization-code/PKCE flow with
38
+ only openid/profile scopes. Access tokens and basic profile details live in browser
39
  page memory, not cookies, localStorage, recipes, or a server session database.
40
+ Only the one-use PKCE verifier/nonce and start time are temporarily saved in
41
+ tab-local sessionStorage to survive the full-page redirect, with a five-minute
42
+ acceptance limit; they are removed on return, error, or a fresh app load. Closing
43
+ the tab loses them. HF handles password entry and authorization. The callback
44
+ briefly carries a one-use authorization code and state in its URL; the app clears
45
+ that URL before loading the OAuth helper. HF/proxy infrastructure handling is outside this
46
  application's deletion guarantee. The app does not request email, billing,
47
  repository access, or persistent bucket access.
48
 
RELEASE_MANIFEST.json CHANGED
@@ -2,13 +2,13 @@
2
  "backend": "cpu+zerogpu",
3
  "files": {
4
  "BETA_TESTING.md": "d511965b9d675a092571f8815348da991ef1ef6be3766266c1f98ff393fd3a4d",
5
- "DATA_POLICY.md": "72bf2be8b87d948501ea44cbdcb2d6d7680f7387ed1c39fd1d3afdf094cf5487",
6
  "FRONTEND_SOURCE.json": "1e31b153757fd42c3bc2abaa1982a44e19a2ed7dc259d32781f36bb9a3ee10b7",
7
  "GODOT_LICENSE": "fb2b3bb82c60f1e76841ba2122eb902f8e39c151476fe21ce10c9a05d1045304",
8
  "IMPORTER_BUILD_RECEIPT.json": "4ba5c533cc617105478e4d85c83f456a7dfa68b1388049624619871ba9e42e9d",
9
  "LICENSE": "53d5851b6440feeae36f72a5f57fcab37858c48b7084765ece4b0c54b9a28ed8",
10
  "README.md": "11abed635d4192b5ea8d8891fc38e3a60989ad5b4696683d0321f3bc12eba20f",
11
- "STORAGE_BOUNDARY.md": "3b91d8db71ece1bb55f38685610d722b162a78af0a602926c16792075e48ef99",
12
  "app.py": "704f789a41846b0a5bc1d3f6f5305d7ac5a6b1719ff77c23585b89fd3e68d55d",
13
  "app_release.py": "7482559c667a665fd3cb6aa8af6c78422a822b515e14589db8306f7fd4860a8b",
14
  "assets/formula_template.json": "80f745df82bb7be45af52785fd582460bcddc63c5a4e1d8375d85b3122db4c6b",
@@ -18,12 +18,11 @@
18
  "client.js": "79a34470942132a4e4a4100ae92b805ff02f7fe344e7962cfcff6a1208cef54a",
19
  "delivery.py": "4f5a4e45df92e5cbfdef6eb449ede9ba26217fe24da376fafddae5cbbb73847a",
20
  "gpu_tickets.py": "c32cb5de751d49d3e1cd970c7d6328a42ebe71602dd9fd3788c872eef42611da",
21
- "hf-auth-callback.html": "07a753a8a70a145d4fc22ae4b259259f4aa202e3d3ae58b9077d271a8ccb7928",
22
- "hf-auth.js": "d39981c98eec48d941a8e8b31b49ba0c44b7debe628c92681dfd26324782e545",
23
  "hf_identity.py": "6993d0238ada835606dbdc3275c17a3fe5a5cac5ad2156ab78230acf2d051eba",
24
  "index.html": "9e5b6bd69dd587f0e5c0f551b291de4d6a729e601916042cde0449ba2162bee8",
25
  "requirements.txt": "9b532747e51bd052e82090ece860d6dafe027407f4512dc74272c148ea104c22",
26
- "server.py": "f0fa001000a55503ac62498105cb9f3a3798051582eac27c46b56ff592d5c734",
27
  "training_config.py": "2b9768876fdc60b16afb45b98023c6c31666e30f7c56f1292f6071775e6da0c5",
28
  "vendor/gradio-client/LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",
29
  "vendor/gradio-client/dist/index.min.js": "fdd776db8633354f118fe2908c980592283968737e6b91dc3aa86d648da5c3fd",
 
2
  "backend": "cpu+zerogpu",
3
  "files": {
4
  "BETA_TESTING.md": "d511965b9d675a092571f8815348da991ef1ef6be3766266c1f98ff393fd3a4d",
5
+ "DATA_POLICY.md": "8a954c46d39783582b2d4fbaa0fa2510e2e13446b0dff0b3c66f8817341b0289",
6
  "FRONTEND_SOURCE.json": "1e31b153757fd42c3bc2abaa1982a44e19a2ed7dc259d32781f36bb9a3ee10b7",
7
  "GODOT_LICENSE": "fb2b3bb82c60f1e76841ba2122eb902f8e39c151476fe21ce10c9a05d1045304",
8
  "IMPORTER_BUILD_RECEIPT.json": "4ba5c533cc617105478e4d85c83f456a7dfa68b1388049624619871ba9e42e9d",
9
  "LICENSE": "53d5851b6440feeae36f72a5f57fcab37858c48b7084765ece4b0c54b9a28ed8",
10
  "README.md": "11abed635d4192b5ea8d8891fc38e3a60989ad5b4696683d0321f3bc12eba20f",
11
+ "STORAGE_BOUNDARY.md": "85442bbaffdf1208a1b7d3359cf1f0a2000a2d1ca46594b3a5fe23d52db41e00",
12
  "app.py": "704f789a41846b0a5bc1d3f6f5305d7ac5a6b1719ff77c23585b89fd3e68d55d",
13
  "app_release.py": "7482559c667a665fd3cb6aa8af6c78422a822b515e14589db8306f7fd4860a8b",
14
  "assets/formula_template.json": "80f745df82bb7be45af52785fd582460bcddc63c5a4e1d8375d85b3122db4c6b",
 
18
  "client.js": "79a34470942132a4e4a4100ae92b805ff02f7fe344e7962cfcff6a1208cef54a",
19
  "delivery.py": "4f5a4e45df92e5cbfdef6eb449ede9ba26217fe24da376fafddae5cbbb73847a",
20
  "gpu_tickets.py": "c32cb5de751d49d3e1cd970c7d6328a42ebe71602dd9fd3788c872eef42611da",
21
+ "hf-auth.js": "743fa2b62c611fcc6064664691e133efb5d163412955b2a9d0a562a428412864",
 
22
  "hf_identity.py": "6993d0238ada835606dbdc3275c17a3fe5a5cac5ad2156ab78230acf2d051eba",
23
  "index.html": "9e5b6bd69dd587f0e5c0f551b291de4d6a729e601916042cde0449ba2162bee8",
24
  "requirements.txt": "9b532747e51bd052e82090ece860d6dafe027407f4512dc74272c148ea104c22",
25
+ "server.py": "dda63ffe8e9ea0191c940fcb996ed790cbb21b8be96e83aeca2a67c33ce45582",
26
  "training_config.py": "2b9768876fdc60b16afb45b98023c6c31666e30f7c56f1292f6071775e6da0c5",
27
  "vendor/gradio-client/LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",
28
  "vendor/gradio-client/dist/index.min.js": "fdd776db8633354f118fe2908c980592283968737e6b91dc3aa86d648da5c3fd",
STORAGE_BOUNDARY.md CHANGED
@@ -12,7 +12,8 @@ same settings contract is used independently of the selected storage policy.
12
  | Single-run output | Memory result store and random job key | Receipt ACK in automatic mode, manual delete, fixed 30-minute expiry, or shutdown |
13
  | Batch intermediates | Same job contract, always automatic | Verified size/hash and deletion ACK before advancement |
14
  | Received candidate / receipt | Browser page memory | Explicit discard, replacement, page reload/close; explicit export to disk |
15
- | Optional HF identity / token / PKCE state | Browser page memory, separate from job/recipe storage | Sign out, expiry, or page reload/close; no app auth cookie or server session database |
 
16
  | User bucket | Not implemented | Must require an explicit opt-in adapter and separate retention policy |
17
 
18
  `recipes.cjs` has no host/network dependency. Its `createStore(storage)` receives
 
12
  | Single-run output | Memory result store and random job key | Receipt ACK in automatic mode, manual delete, fixed 30-minute expiry, or shutdown |
13
  | Batch intermediates | Same job contract, always automatic | Verified size/hash and deletion ACK before advancement |
14
  | Received candidate / receipt | Browser page memory | Explicit discard, replacement, page reload/close; explicit export to disk |
15
+ | Optional HF identity / token | Browser page memory, separate from job/recipe storage | Sign out, expiry, or page reload/close; no app auth cookie or server session database |
16
+ | One-use HF PKCE verifier/nonce | Tab-local sessionStorage for the sign-in redirect only | Five-minute acceptance limit; removed on return/error/fresh app load or tab close; never stores access tokens or files |
17
  | User bucket | Not implemented | Must require an explicit opt-in adapter and separate retention policy |
18
 
19
  `recipes.cjs` has no host/network dependency. Its `createStore(storage)` receives
hf-auth.js CHANGED
@@ -1,19 +1,20 @@
1
  'use strict';
2
- // Official HF PKCE helpers; nonce/verifier and visitor token live in page RAM only.
 
3
  (() => {
4
- const node=id=>document.getElementById(id);
5
- let session=null,pending=null,locked=false,ready=false,config=null,exchanging=false;
6
  const message=text=>{node('hfIdentity').textContent=text;};
7
  function render(){
8
- node('hfSignIn').disabled=locked||!!pending||!ready;
9
- node('hfSignOut').disabled=locked||!!pending||!session;
10
- node('hfVerify').disabled=locked||!!pending||!session;
11
  }
12
  function clear(){session=null;window.dispatchEvent(new Event('hf-auth-change'));render();}
13
  window.HFAuth={
14
  setBusy(value){locked=value;render();},
15
  credentials(){
16
- if(pending||exchanging)throw Error('Complete or cancel HF sign-in before starting GPU');
17
  if(session && session.accessTokenExpiresAt.getTime()<=Date.now()){
18
  clear();message('HF sign-in expired. Sign in again before an account GPU request.');
19
  throw Error('HF sign-in expired; sign in again');
@@ -32,49 +33,52 @@
32
  catch{message('Queue identity check failed. No GPU request started. Sign in again or use CPU.');}
33
  };
34
  node('hfSignIn').onclick=async()=>{
35
- if(locked||pending||!ready)return;
36
- const popup=window.open('about:blank','ald-hf-sign-in','popup,width=620,height=760');
37
- if(!popup){message('Sign-in window blocked. Allow this sign-in popup or use anonymous CPU.');return;}
38
- const state={};
39
- pending={popup,state,started:Date.now()};render();
40
  try{
 
41
  const helpers=await import('/hf-oauth.js');
42
  const url=await helpers.oauthLoginUrl({clientId:config.client_id,scopes:'openid profile',
43
- redirectUrl:location.origin+'/auth/callback',localStorage:state});
44
- if(!pending || pending.popup!==popup)return;
45
- popup.location.href=url;message('Complete the optional sign-in on Hugging Face. No password or personal token is entered here.');
 
46
  }catch{
47
- pending=null;popup.close();render();message('HF sign-in could not start. CPU remains available.');
48
  }
49
  };
50
- window.addEventListener('message',async event=>{
51
- if(!pending||event.origin!==location.origin||event.source!==pending.popup||event.data?.type!=='hf-oauth-return')return;
52
- const flow=pending;pending=null;ready=false;exchanging=true;render();
53
- try{
54
- const returned=new URL(event.data.url);
55
- if(returned.origin!==location.origin||returned.pathname!=='/auth/callback')throw Error();
 
 
 
 
56
  const helpers=await import('/hf-oauth.js');
57
- const result=await helpers.oauthHandleRedirect({redirectedUrl:returned.href,
58
- nonce:flow.state.nonce,codeVerifier:flow.state.codeVerifier});
59
  const scopes=new Set(result.scope.split(/\s+/));
60
  if([...scopes].some(scope=>!['openid','profile'].includes(scope)))throw Error();
61
  if(!result.accessToken||!result.userInfo.preferred_username||result.accessTokenExpiresAt.getTime()<=Date.now())throw Error();
62
  session=result;window.dispatchEvent(new Event('hf-auth-change'));
63
  message('Signed in with HF: '+result.userInfo.preferred_username+'. Verify queue identity before GPU use.');
64
- }catch{
65
- clear();message('HF sign-in was cancelled or could not be verified. No GPU request started; CPU remains available.');
66
- }finally{
67
- flow.state.nonce=null;flow.state.codeVerifier=null;ready=true;exchanging=false;render();
68
  }
69
- });
 
 
 
70
  setInterval(()=>{
71
- if(pending&&(pending.popup.closed||Date.now()-pending.started>300000)){
72
- pending.popup.close();pending=null;render();message('HF sign-in window closed or timed out. CPU remains available.');
73
  }
74
  },1000);
75
- fetch('/api/auth/config',{cache:'no-store',credentials:'omit'}).then(async response=>{
76
- if(!response.ok)throw Error();config=await response.json();ready=config.enabled===true;render();
77
- message(ready?'Optional: sign in with HF to identify account GPU requests. CPU needs no sign-in. Token stays in this page’s memory until sign-out, expiry, or page close.':
78
- 'HF sign-in is not configured on this deployment. CPU remains anonymous.');
79
- }).catch(()=>{message('HF sign-in unavailable. CPU remains anonymous.');render();});
80
  })();
 
1
  'use strict';
2
+ // Official HF PKCE redirect helpers. Only the one-use verifier crosses navigation;
3
+ // account access tokens and profile details stay in this page's memory.
4
  (() => {
5
+ const node=id=>document.getElementById(id),flowKey='tiga.ald.hf.pkce.v1';
6
+ let session=null,locked=false,ready=false,config=null,exchanging=false;
7
  const message=text=>{node('hfIdentity').textContent=text;};
8
  function render(){
9
+ node('hfSignIn').disabled=locked||exchanging||!ready;
10
+ node('hfSignOut').disabled=locked||exchanging||!session;
11
+ node('hfVerify').disabled=locked||exchanging||!session;
12
  }
13
  function clear(){session=null;window.dispatchEvent(new Event('hf-auth-change'));render();}
14
  window.HFAuth={
15
  setBusy(value){locked=value;render();},
16
  credentials(){
17
+ if(exchanging)throw Error('Complete HF sign-in before starting GPU');
18
  if(session && session.accessTokenExpiresAt.getTime()<=Date.now()){
19
  clear();message('HF sign-in expired. Sign in again before an account GPU request.');
20
  throw Error('HF sign-in expired; sign in again');
 
33
  catch{message('Queue identity check failed. No GPU request started. Sign in again or use CPU.');}
34
  };
35
  node('hfSignIn').onclick=async()=>{
36
+ if(locked||exchanging||!ready)return;
37
+ if((node('track').files.length||node('network').files.length) &&
38
+ !confirm('HF sign-in navigates away. Selected files and unsaved editor changes are lost; saved recipes remain. Continue, then reselect files?'))return;
39
+ exchanging=true;render();
 
40
  try{
41
+ const state={};
42
  const helpers=await import('/hf-oauth.js');
43
  const url=await helpers.oauthLoginUrl({clientId:config.client_id,scopes:'openid profile',
44
+ redirectUrl:location.origin+'/',localStorage:state});
45
+ sessionStorage.setItem(flowKey,JSON.stringify({...state,started:Date.now()}));
46
+ message('Opening Hugging Face sign-in. No password or personal token is entered here.');
47
+ location.href=url;
48
  }catch{
49
+ exchanging=false;render();message('HF sign-in could not start. Tab-local sign-in storage is required; CPU remains available.');
50
  }
51
  };
52
+ const returned=location.href,params=new URL(returned).searchParams;
53
+ const isCallback=params.has('code')||params.has('error');
54
+ if(isCallback){history.replaceState(null,'','/');exchanging=true;}
55
+ fetch('/api/auth/config',{cache:'no-store',credentials:'omit'}).then(async response=>{
56
+ if(!response.ok)throw Error();config=await response.json();ready=config.enabled===true;
57
+ if(isCallback){
58
+ let flow;
59
+ try{flow=JSON.parse(sessionStorage.getItem(flowKey));}
60
+ finally{sessionStorage.removeItem(flowKey);}
61
+ if(!ready||!flow?.nonce||!flow?.codeVerifier||Date.now()-flow.started>300000||flow.started>Date.now())throw Error();
62
  const helpers=await import('/hf-oauth.js');
63
+ const result=await helpers.oauthHandleRedirect({redirectedUrl:returned,nonce:flow.nonce,codeVerifier:flow.codeVerifier});
64
+ flow.nonce=null;flow.codeVerifier=null;
65
  const scopes=new Set(result.scope.split(/\s+/));
66
  if([...scopes].some(scope=>!['openid','profile'].includes(scope)))throw Error();
67
  if(!result.accessToken||!result.userInfo.preferred_username||result.accessTokenExpiresAt.getTime()<=Date.now())throw Error();
68
  session=result;window.dispatchEvent(new Event('hf-auth-change'));
69
  message('Signed in with HF: '+result.userInfo.preferred_username+'. Verify queue identity before GPU use.');
70
+ }else{
71
+ sessionStorage.removeItem(flowKey);
72
+ message(ready?'Optional: sign in with HF before choosing files. CPU needs no sign-in. Token stays in page memory; only a one-use sign-in verifier crosses the redirect in tab-local storage.':
73
+ 'HF sign-in is not configured on this deployment. CPU remains anonymous.');
74
  }
75
+ }).catch(()=>{
76
+ try{sessionStorage.removeItem(flowKey);}catch{}
77
+ clear();message('HF sign-in unavailable, cancelled, expired, or not verified. No GPU request started; CPU remains anonymous.');
78
+ }).finally(()=>{exchanging=false;render();});
79
  setInterval(()=>{
80
+ if(session&&session.accessTokenExpiresAt.getTime()<=Date.now()){
81
+ clear();message('HF sign-in expired. Sign in again to identify account GPU requests. CPU remains available.');
82
  }
83
  },1000);
 
 
 
 
 
84
  })();
server.py CHANGED
@@ -19,10 +19,6 @@ def create_app(worker=run_reported,store=None,gpu_worker=None):
19
  @app.get('/api/auth/config')
20
  def auth_config():
21
  return JSONResponse(oauth_config(),headers={'Cache-Control':'no-store'})
22
- @app.get('/auth/callback')
23
- def auth_callback():
24
- return FileResponse(ROOT/'hf-auth-callback.html',media_type='text/html',
25
- headers={'Cache-Control':'no-store','Referrer-Policy':'no-referrer'})
26
  @app.get('/hf-auth.js')
27
  def auth_script(): return FileResponse(ROOT/'hf-auth.js',media_type='text/javascript')
28
  @app.get('/hf-oauth.js')
@@ -36,7 +32,8 @@ def create_app(worker=run_reported,store=None,gpu_worker=None):
36
  # iframe subresource access can fail before an external script runs.
37
  script=(ROOT/'client.js').read_text().replace('</script','<\\/script')
38
  return HTMLResponse((ROOT/'index.html').read_text().replace(
39
- '<script src="/client.js"></script>','<script>'+script+'</script>'))
 
40
  @app.get('/client.js')
41
  def script(): return FileResponse(ROOT/'client.js',media_type='text/javascript')
42
  @app.get('/policy')
 
19
  @app.get('/api/auth/config')
20
  def auth_config():
21
  return JSONResponse(oauth_config(),headers={'Cache-Control':'no-store'})
 
 
 
 
22
  @app.get('/hf-auth.js')
23
  def auth_script(): return FileResponse(ROOT/'hf-auth.js',media_type='text/javascript')
24
  @app.get('/hf-oauth.js')
 
32
  # iframe subresource access can fail before an external script runs.
33
  script=(ROOT/'client.js').read_text().replace('</script','<\\/script')
34
  return HTMLResponse((ROOT/'index.html').read_text().replace(
35
+ '<script src="/client.js"></script>','<script>'+script+'</script>'),
36
+ headers={'Referrer-Policy':'no-referrer','Cache-Control':'no-store'})
37
  @app.get('/client.js')
38
  def script(): return FileResponse(ROOT/'client.js',media_type='text/javascript')
39
  @app.get('/policy')