[{"data":1,"prerenderedAt":2009},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fauthenticate-to-microsoft-graph-with-msal-for-excel-automation":3,"surround:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fauthenticate-to-microsoft-graph-with-msal-for-excel-automation":1979,"ancestors:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fauthenticate-to-microsoft-graph-with-msal-for-excel-automation":1988,"children:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fauthenticate-to-microsoft-graph-with-msal-for-excel-automation":1994,"related:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fauthenticate-to-microsoft-graph-with-msal-for-excel-automation":1995},{"id":4,"title":5,"body":6,"dateModified":1947,"datePublished":1947,"description":1948,"extension":1949,"faq":1950,"keyword":1959,"kind":1960,"meta":1961,"navigation":266,"path":1970,"seo":1971,"slug":1973,"stem":1974,"topics":1975,"type":1977,"__hash__":1978},"docs\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fauthenticate-to-microsoft-graph-with-msal-for-excel-automation\u002Findex.md","Authenticate to Microsoft Graph with MSAL for Excel Automation",{"type":7,"value":8,"toc":1934},"minimark",[9,38,135,140,167,174,214,229,233,236,483,500,504,515,938,949,1045,1049,1060,1224,1227,1231,1234,1386,1400,1404,1407,1588,1594,1702,1729,1733,1837,1841,1855,1859,1862,1866,1872,1878,1884,1890,1894,1930],[10,11,12,13,17,18,22,23,26,27,31,32,37],"p",{},"Every Microsoft Graph call needs a bearer token, and for Excel automation the kind of token matters more\nthan usual. Graph has two quite different ways to touch a workbook in OneDrive or SharePoint. The ",[14,15,16],"strong",{},"file\nendpoints"," download and upload the ",[19,20,21],"code",{},".xlsx"," as bytes, and they work with an app-only token from a client\nsecret. The ",[14,24,25],{},"workbook endpoints"," — reading and writing ranges, appending table rows, recalculating —\noperate on the workbook inside Excel's online service, and Microsoft's reference pages list their\napplication permissions as ",[28,29,30],"em",{},"not supported",": they need a token issued on behalf of a signed-in user. This\nguide, part of ",[33,34,36],"a",{"href":35},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002F","Automating Excel Online with Microsoft Graph",",\nsets up both, including the token cache that lets a delegated job run on a schedule.",[39,40,48,49,48,53,48,57,48,64,48,73,48,80,48,87,48,92,48,96,48,100,48,104,48,109,48,113,48,117,48,120,48,123,48,126,48,129],"svg",{"viewBox":41,"role":42,"ariaLabelledBy":43,"xmlns":46,"style":47},"0 0 760 214","img",[44,45],"auth-kinds-t","auth-kinds-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[50,51,52],"title",{"id":44},"Which token each kind of Graph Excel call needs",[54,55,56],"desc",{"id":45},"File endpoints that download or upload workbook bytes accept application permissions with client credentials. Workbook endpoints for ranges, tables and calculation accept only delegated permissions from a signed-in user.",[58,59],"rect",{"x":60,"y":60,"width":61,"height":62,"fill":63},"0","760","214","#ffffff",[58,65],{"x":66,"y":66,"width":67,"height":68,"rx":69,"fill":70,"stroke":71,"style":72},"20","350","150","14","#d9f4f1","var(--teal,#0f9488)","stroke-width:2px",[74,75,79],"text",{"x":76,"y":77,"style":78},"195","46","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","file endpoints",[81,82],"line",{"x1":83,"y1":84,"x2":85,"y2":84,"stroke":71,"style":86},"36","56","354","stroke-width:1px",[74,88,91],{"x":76,"y":89,"style":90},"78","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","GET …\u002Fcontent, PUT …\u002Fcontent",[74,93,95],{"x":76,"y":94,"style":90},"101","bytes in, bytes out",[74,97,99],{"x":76,"y":98,"style":90},"124","app-only token works",[74,101,103],{"x":76,"y":102,"style":90},"147","client secret or certificate",[58,105],{"x":106,"y":66,"width":67,"height":68,"rx":69,"fill":107,"stroke":108,"style":72},"390","#f0f4ff","var(--brand,#5b5cf0)",[74,110,25],{"x":111,"y":77,"style":112},"565","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[81,114],{"x1":115,"y1":84,"x2":116,"y2":84,"stroke":108,"style":86},"406","724",[74,118,119],{"x":111,"y":89,"style":90},"…\u002Fworkbook\u002Fworksheets\u002F…\u002Frange",[74,121,122],{"x":111,"y":94,"style":90},"…\u002Ftables\u002F…\u002Frows\u002Fadd",[74,124,125],{"x":111,"y":98,"style":90},"delegated token only",[74,127,128],{"x":111,"y":102,"style":90},"Files.ReadWrite as a user",[74,130,134],{"x":131,"y":132,"style":133},"380","200","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","decide per call; one job often needs both",[136,137,139],"h2",{"id":138},"prerequisites","Prerequisites",[141,142,147],"pre",{"className":143,"code":144,"language":145,"meta":146,"style":146},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install msal requests\n","bash","",[19,148,149],{"__ignoreMap":146},[150,151,153,157,161,164],"span",{"class":81,"line":152},1,[150,154,156],{"class":155},"sMTad","pip",[150,158,160],{"class":159},"srMev"," install",[150,162,163],{"class":159}," msal",[150,165,166],{"class":159}," requests\n",[10,168,169,170,173],{},"You also need an app registration in Microsoft Entra ID (the tenant's admin centre, under ",[14,171,172],{},"App registrations","):",[175,176,177,200],"ul",{},[178,179,180,183,184,187,188,191,192,195,196,199],"li",{},[14,181,182],{},"Delegated access"," — add the Microsoft Graph delegated permission ",[19,185,186],{},"Files.ReadWrite"," (or ",[19,189,190],{},"Files.ReadWrite.All"," if the job\nmust reach files other people shared rather than the signed-in user's own), and under ",[14,193,194],{},"Authentication"," enable\n",[14,197,198],{},"Allow public client flows"," so the device code flow is permitted.",[178,201,202,205,206,209,210,213],{},[14,203,204],{},"App-only access"," — add the application permission you need for file content, such as ",[19,207,208],{},"Sites.Selected"," or\n",[19,211,212],{},"Files.Read.All",", grant admin consent, and create a client secret or upload a certificate.",[10,215,216,217,220,221,224,225,228],{},"Keep the tenant ID, client ID and any secret in environment variables, never in the script. The examples read\n",[19,218,219],{},"GRAPH_TENANT_ID",", ",[19,222,223],{},"GRAPH_CLIENT_ID"," and, for app-only, ",[19,226,227],{},"GRAPH_CLIENT_SECRET",".",[136,230,232],{"id":231},"step-1-sign-in-once-with-the-device-code-flow","Step 1 — Sign in once with the device code flow",[10,234,235],{},"The device code flow suits scripts and servers because it needs no browser on the machine running the code. MSAL asks\nEntra ID for a short code, prints a message telling a person to open a sign-in page on any device and type it, and waits\nuntil they have done so.",[141,237,241],{"className":238,"code":239,"language":240,"meta":146,"style":146},"language-python shiki shiki-themes github-light github-dark-high-contrast","import os\nimport msal\n\nTENANT_ID = os.environ.get(\"GRAPH_TENANT_ID\", \"contoso.onmicrosoft.com\")\nCLIENT_ID = os.environ.get(\"GRAPH_CLIENT_ID\", \"00000000-0000-0000-0000-000000000000\")\nAUTHORITY = f\"https:\u002F\u002Flogin.microsoftonline.com\u002F{TENANT_ID}\"\nSCOPES = [\"Files.ReadWrite\"]\n\ndef sign_in_interactively(app: msal.PublicClientApplication) -> dict:\n    flow = app.initiate_device_flow(scopes=SCOPES)\n    if \"user_code\" not in flow:\n        raise RuntimeError(f\"could not start device flow: {flow.get('error_description')}\")\n    print(flow[\"message\"])            # \"To sign in, use a web browser to open … and enter the code …\"\n    return app.acquire_token_by_device_flow(flow)\n","python",[19,242,243,253,261,268,292,312,338,355,360,379,401,419,455,474],{"__ignoreMap":146},[150,244,245,249],{"class":81,"line":152},[150,246,248],{"class":247},"s-kum","import",[150,250,252],{"class":251},"skGVy"," os\n",[150,254,256,258],{"class":81,"line":255},2,[150,257,248],{"class":247},[150,259,260],{"class":251}," msal\n",[150,262,264],{"class":81,"line":263},3,[150,265,267],{"emptyLinePlaceholder":266},true,"\n",[150,269,271,275,278,281,284,286,289],{"class":81,"line":270},4,[150,272,274],{"class":273},"sP0c6","TENANT_ID",[150,276,277],{"class":247}," =",[150,279,280],{"class":251}," os.environ.get(",[150,282,283],{"class":159},"\"GRAPH_TENANT_ID\"",[150,285,220],{"class":251},[150,287,288],{"class":159},"\"contoso.onmicrosoft.com\"",[150,290,291],{"class":251},")\n",[150,293,295,298,300,302,305,307,310],{"class":81,"line":294},5,[150,296,297],{"class":273},"CLIENT_ID",[150,299,277],{"class":247},[150,301,280],{"class":251},[150,303,304],{"class":159},"\"GRAPH_CLIENT_ID\"",[150,306,220],{"class":251},[150,308,309],{"class":159},"\"00000000-0000-0000-0000-000000000000\"",[150,311,291],{"class":251},[150,313,315,318,320,323,326,330,332,335],{"class":81,"line":314},6,[150,316,317],{"class":273},"AUTHORITY",[150,319,277],{"class":247},[150,321,322],{"class":247}," f",[150,324,325],{"class":159},"\"https:\u002F\u002Flogin.microsoftonline.com\u002F",[150,327,329],{"class":328},"sSjpA","{",[150,331,274],{"class":273},[150,333,334],{"class":328},"}",[150,336,337],{"class":159},"\"\n",[150,339,341,344,346,349,352],{"class":81,"line":340},7,[150,342,343],{"class":273},"SCOPES",[150,345,277],{"class":247},[150,347,348],{"class":251}," [",[150,350,351],{"class":159},"\"Files.ReadWrite\"",[150,353,354],{"class":251},"]\n",[150,356,358],{"class":81,"line":357},8,[150,359,267],{"emptyLinePlaceholder":266},[150,361,363,366,370,373,376],{"class":81,"line":362},9,[150,364,365],{"class":247},"def",[150,367,369],{"class":368},"s_Opv"," sign_in_interactively",[150,371,372],{"class":251},"(app: msal.PublicClientApplication) -> ",[150,374,375],{"class":273},"dict",[150,377,378],{"class":251},":\n",[150,380,382,385,388,391,395,397,399],{"class":81,"line":381},10,[150,383,384],{"class":251},"    flow ",[150,386,387],{"class":247},"=",[150,389,390],{"class":251}," app.initiate_device_flow(",[150,392,394],{"class":393},"sa561","scopes",[150,396,387],{"class":247},[150,398,343],{"class":273},[150,400,291],{"class":251},[150,402,404,407,410,413,416],{"class":81,"line":403},11,[150,405,406],{"class":247},"    if",[150,408,409],{"class":159}," \"user_code\"",[150,411,412],{"class":247}," not",[150,414,415],{"class":247}," in",[150,417,418],{"class":251}," flow:\n",[150,420,422,425,428,431,434,437,439,442,445,448,450,453],{"class":81,"line":421},12,[150,423,424],{"class":247},"        raise",[150,426,427],{"class":273}," RuntimeError",[150,429,430],{"class":251},"(",[150,432,433],{"class":247},"f",[150,435,436],{"class":159},"\"could not start device flow: ",[150,438,329],{"class":328},[150,440,441],{"class":251},"flow.get(",[150,443,444],{"class":159},"'error_description'",[150,446,447],{"class":251},")",[150,449,334],{"class":328},[150,451,452],{"class":159},"\"",[150,454,291],{"class":251},[150,456,458,461,464,467,470],{"class":81,"line":457},13,[150,459,460],{"class":273},"    print",[150,462,463],{"class":251},"(flow[",[150,465,466],{"class":159},"\"message\"",[150,468,469],{"class":251},"])            ",[150,471,473],{"class":472},"s-wDw","# \"To sign in, use a web browser to open … and enter the code …\"\n",[150,475,477,480],{"class":81,"line":476},14,[150,478,479],{"class":247},"    return",[150,481,482],{"class":251}," app.acquire_token_by_device_flow(flow)\n",[10,484,485,486,220,489,492,493,496,497,499],{},"Request only the resource scopes you need. MSAL adds ",[19,487,488],{},"openid",[19,490,491],{},"profile"," and ",[19,494,495],{},"offline_access"," itself, and\n",[19,498,495],{}," is what makes Entra ID return a refresh token — the piece that lets later runs happen without a person.",[136,501,503],{"id":502},"step-2-persist-the-token-cache-between-runs","Step 2 — Persist the token cache between runs",[10,505,506,507,510,511,514],{},"By default MSAL keeps tokens in memory, so a new process would need a new sign-in. A ",[19,508,509],{},"SerializableTokenCache"," written to\na file keeps the account and its refresh token, and ",[19,512,513],{},"acquire_token_silent"," then returns a valid access token — refreshing\nit behind the scenes when the old one has expired — without any prompt.",[141,516,518],{"className":238,"code":517,"language":240,"meta":146,"style":146},"import json\nimport stat\nfrom pathlib import Path\n\nCACHE_PATH = Path(os.environ.get(\"GRAPH_TOKEN_CACHE\", \".graph_token_cache.json\"))\n\ndef load_cache() -> msal.SerializableTokenCache:\n    cache = msal.SerializableTokenCache()\n    if CACHE_PATH.exists():\n        cache.deserialize(CACHE_PATH.read_text())\n    return cache\n\ndef save_cache(cache: msal.SerializableTokenCache) -> None:\n    if cache.has_state_changed:\n        CACHE_PATH.write_text(cache.serialize())\n        CACHE_PATH.chmod(stat.S_IRUSR | stat.S_IWUSR)     # readable by this user only\n\ndef get_delegated_token(interactive: bool = False) -> str:\n    cache = load_cache()\n    app = msal.PublicClientApplication(CLIENT_ID, authority=AUTHORITY, token_cache=cache)\n    result = None\n    accounts = app.get_accounts()\n    if accounts:\n        result = app.acquire_token_silent(SCOPES, account=accounts[0])\n    if not result:\n        if not interactive:\n            raise RuntimeError(\"no cached Graph sign-in; run once with interactive=True\")\n        result = sign_in_interactively(app)\n    save_cache(cache)\n    if \"access_token\" not in result:\n        raise RuntimeError(f\"token request failed: {result.get('error')}: {result.get('error_description')}\")\n    return result[\"access_token\"]\n",[19,519,520,527,534,547,551,572,576,586,596,606,616,623,627,642,649,658,684,689,716,726,758,769,780,788,816,826,837,852,862,868,882,925],{"__ignoreMap":146},[150,521,522,524],{"class":81,"line":152},[150,523,248],{"class":247},[150,525,526],{"class":251}," json\n",[150,528,529,531],{"class":81,"line":255},[150,530,248],{"class":247},[150,532,533],{"class":251}," stat\n",[150,535,536,539,542,544],{"class":81,"line":263},[150,537,538],{"class":247},"from",[150,540,541],{"class":251}," pathlib ",[150,543,248],{"class":247},[150,545,546],{"class":251}," Path\n",[150,548,549],{"class":81,"line":270},[150,550,267],{"emptyLinePlaceholder":266},[150,552,553,556,558,561,564,566,569],{"class":81,"line":294},[150,554,555],{"class":273},"CACHE_PATH",[150,557,277],{"class":247},[150,559,560],{"class":251}," Path(os.environ.get(",[150,562,563],{"class":159},"\"GRAPH_TOKEN_CACHE\"",[150,565,220],{"class":251},[150,567,568],{"class":159},"\".graph_token_cache.json\"",[150,570,571],{"class":251},"))\n",[150,573,574],{"class":81,"line":314},[150,575,267],{"emptyLinePlaceholder":266},[150,577,578,580,583],{"class":81,"line":340},[150,579,365],{"class":247},[150,581,582],{"class":368}," load_cache",[150,584,585],{"class":251},"() -> msal.SerializableTokenCache:\n",[150,587,588,591,593],{"class":81,"line":357},[150,589,590],{"class":251},"    cache ",[150,592,387],{"class":247},[150,594,595],{"class":251}," msal.SerializableTokenCache()\n",[150,597,598,600,603],{"class":81,"line":362},[150,599,406],{"class":247},[150,601,602],{"class":273}," CACHE_PATH",[150,604,605],{"class":251},".exists():\n",[150,607,608,611,613],{"class":81,"line":381},[150,609,610],{"class":251},"        cache.deserialize(",[150,612,555],{"class":273},[150,614,615],{"class":251},".read_text())\n",[150,617,618,620],{"class":81,"line":403},[150,619,479],{"class":247},[150,621,622],{"class":251}," cache\n",[150,624,625],{"class":81,"line":421},[150,626,267],{"emptyLinePlaceholder":266},[150,628,629,631,634,637,640],{"class":81,"line":457},[150,630,365],{"class":247},[150,632,633],{"class":368}," save_cache",[150,635,636],{"class":251},"(cache: msal.SerializableTokenCache) -> ",[150,638,639],{"class":273},"None",[150,641,378],{"class":251},[150,643,644,646],{"class":81,"line":476},[150,645,406],{"class":247},[150,647,648],{"class":251}," cache.has_state_changed:\n",[150,650,652,655],{"class":81,"line":651},15,[150,653,654],{"class":273},"        CACHE_PATH",[150,656,657],{"class":251},".write_text(cache.serialize())\n",[150,659,661,663,666,669,672,675,678,681],{"class":81,"line":660},16,[150,662,654],{"class":273},[150,664,665],{"class":251},".chmod(stat.",[150,667,668],{"class":273},"S_IRUSR",[150,670,671],{"class":247}," |",[150,673,674],{"class":251}," stat.",[150,676,677],{"class":273},"S_IWUSR",[150,679,680],{"class":251},")     ",[150,682,683],{"class":472},"# readable by this user only\n",[150,685,687],{"class":81,"line":686},17,[150,688,267],{"emptyLinePlaceholder":266},[150,690,692,694,697,700,703,705,708,711,714],{"class":81,"line":691},18,[150,693,365],{"class":247},[150,695,696],{"class":368}," get_delegated_token",[150,698,699],{"class":251},"(interactive: ",[150,701,702],{"class":273},"bool",[150,704,277],{"class":247},[150,706,707],{"class":273}," False",[150,709,710],{"class":251},") -> ",[150,712,713],{"class":273},"str",[150,715,378],{"class":251},[150,717,719,721,723],{"class":81,"line":718},19,[150,720,590],{"class":251},[150,722,387],{"class":247},[150,724,725],{"class":251}," load_cache()\n",[150,727,729,732,734,737,739,741,744,746,748,750,753,755],{"class":81,"line":728},20,[150,730,731],{"class":251},"    app ",[150,733,387],{"class":247},[150,735,736],{"class":251}," msal.PublicClientApplication(",[150,738,297],{"class":273},[150,740,220],{"class":251},[150,742,743],{"class":393},"authority",[150,745,387],{"class":247},[150,747,317],{"class":273},[150,749,220],{"class":251},[150,751,752],{"class":393},"token_cache",[150,754,387],{"class":247},[150,756,757],{"class":251},"cache)\n",[150,759,761,764,766],{"class":81,"line":760},21,[150,762,763],{"class":251},"    result ",[150,765,387],{"class":247},[150,767,768],{"class":273}," None\n",[150,770,772,775,777],{"class":81,"line":771},22,[150,773,774],{"class":251},"    accounts ",[150,776,387],{"class":247},[150,778,779],{"class":251}," app.get_accounts()\n",[150,781,783,785],{"class":81,"line":782},23,[150,784,406],{"class":247},[150,786,787],{"class":251}," accounts:\n",[150,789,791,794,796,799,801,803,806,808,811,813],{"class":81,"line":790},24,[150,792,793],{"class":251},"        result ",[150,795,387],{"class":247},[150,797,798],{"class":251}," app.acquire_token_silent(",[150,800,343],{"class":273},[150,802,220],{"class":251},[150,804,805],{"class":393},"account",[150,807,387],{"class":247},[150,809,810],{"class":251},"accounts[",[150,812,60],{"class":273},[150,814,815],{"class":251},"])\n",[150,817,819,821,823],{"class":81,"line":818},25,[150,820,406],{"class":247},[150,822,412],{"class":247},[150,824,825],{"class":251}," result:\n",[150,827,829,832,834],{"class":81,"line":828},26,[150,830,831],{"class":247},"        if",[150,833,412],{"class":247},[150,835,836],{"class":251}," interactive:\n",[150,838,840,843,845,847,850],{"class":81,"line":839},27,[150,841,842],{"class":247},"            raise",[150,844,427],{"class":273},[150,846,430],{"class":251},[150,848,849],{"class":159},"\"no cached Graph sign-in; run once with interactive=True\"",[150,851,291],{"class":251},[150,853,855,857,859],{"class":81,"line":854},28,[150,856,793],{"class":251},[150,858,387],{"class":247},[150,860,861],{"class":251}," sign_in_interactively(app)\n",[150,863,865],{"class":81,"line":864},29,[150,866,867],{"class":251},"    save_cache(cache)\n",[150,869,871,873,876,878,880],{"class":81,"line":870},30,[150,872,406],{"class":247},[150,874,875],{"class":159}," \"access_token\"",[150,877,412],{"class":247},[150,879,415],{"class":247},[150,881,825],{"class":251},[150,883,885,887,889,891,893,896,898,901,904,906,908,911,913,915,917,919,921,923],{"class":81,"line":884},31,[150,886,424],{"class":247},[150,888,427],{"class":273},[150,890,430],{"class":251},[150,892,433],{"class":247},[150,894,895],{"class":159},"\"token request failed: ",[150,897,329],{"class":328},[150,899,900],{"class":251},"result.get(",[150,902,903],{"class":159},"'error'",[150,905,447],{"class":251},[150,907,334],{"class":328},[150,909,910],{"class":159},": ",[150,912,329],{"class":328},[150,914,900],{"class":251},[150,916,444],{"class":159},[150,918,447],{"class":251},[150,920,334],{"class":328},[150,922,452],{"class":159},[150,924,291],{"class":251},[150,926,928,930,933,936],{"class":81,"line":927},32,[150,929,479],{"class":247},[150,931,932],{"class":251}," result[",[150,934,935],{"class":159},"\"access_token\"",[150,937,354],{"class":251},[10,939,940,941,944,945,948],{},"The ",[19,942,943],{},"interactive"," flag is the detail that keeps scheduled jobs healthy. A cron job must never sit waiting for somebody to\ntype a device code at 06:00; it should fail immediately with a message that says what to do. Run the script once by hand\nwith ",[19,946,947],{},"interactive=True"," to seed the cache, then schedule it.",[39,950,48,955,48,958,48,961,48,964,48,970,48,976,48,981,48,986,48,991,48,994,48,999,48,1002,48,1006,48,1009,48,1012,48,1016,48,1019,48,1023,48,1027,48,1030,48,1033,48,1038,48,1041],{"viewBox":951,"role":42,"ariaLabelledBy":952,"xmlns":46,"style":47},"0 0 760 156",[953,954],"auth-cache-t","auth-cache-d",[50,956,957],{"id":953},"How a scheduled job gets a delegated token without a prompt",[54,959,960],{"id":954},"The job loads the token cache from disk, asks MSAL for a token silently, MSAL uses the refresh token when the access token has expired, and the updated cache is written back. Only when no cached account exists does the job stop and ask for an interactive sign-in.",[58,962],{"x":60,"y":60,"width":61,"height":963,"fill":63},"156",[58,965],{"x":66,"y":66,"width":68,"height":966,"rx":967,"fill":968,"stroke":969,"style":72},"86","12","#e7ebef","var(--line,#cdd5e6)",[74,971,975],{"x":972,"y":973,"style":974},"95","50","font-size:14px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","load cache",[74,977,980],{"x":972,"y":978,"style":979},"72","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","file on disk",[81,982],{"x1":983,"y1":984,"x2":985,"y2":984,"stroke":108,"style":72},"175","63","204",[987,988],"polygon",{"points":989,"fill":990},"207,63 198,58 198,68","#5b5cf0",[58,992],{"x":993,"y":66,"width":68,"height":966,"rx":967,"fill":107,"stroke":108,"style":72},"210",[74,995,998],{"x":996,"y":973,"style":997},"285","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","acquire_silent",[74,1000,1001],{"x":996,"y":978,"style":90},"cached account",[81,1003],{"x1":1004,"y1":984,"x2":1005,"y2":984,"stroke":108,"style":72},"365","394",[987,1007],{"points":1008,"fill":990},"397,63 388,58 388,68",[58,1010],{"x":1011,"y":66,"width":68,"height":966,"rx":967,"fill":107,"stroke":108,"style":72},"400",[74,1013,1015],{"x":1014,"y":973,"style":997},"475","refresh",[74,1017,1018],{"x":1014,"y":978,"style":90},"refresh token",[74,1020,1022],{"x":1014,"y":1021,"style":90},"92","used by MSAL",[81,1024],{"x1":1025,"y1":984,"x2":1026,"y2":984,"stroke":108,"style":72},"555","584",[987,1028],{"points":1029,"fill":990},"587,63 578,58 578,68",[58,1031],{"x":1032,"y":66,"width":68,"height":966,"rx":967,"fill":70,"stroke":71,"style":72},"590",[74,1034,1037],{"x":1035,"y":973,"style":1036},"665","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","save cache",[74,1039,1040],{"x":1035,"y":978,"style":90},"only if changed",[74,1042,1044],{"x":131,"y":1043,"style":133},"140","no cached account: fail fast and ask for one manual sign-in",[136,1046,1048],{"id":1047},"step-3-treat-the-cache-file-as-a-credential","Step 3 — Treat the cache file as a credential",[10,1050,1051,1052,1055,1056,1059],{},"The cache holds a refresh token that can obtain new access tokens for the signed-in account, so it deserves the same care\nas a password. The ",[19,1053,1054],{},"chmod"," above restricts it to the owning user on Linux and macOS. On a shared server, keep it on an\nencrypted volume or use a platform keyring through the ",[19,1057,1058],{},"msal-extensions"," package, which stores the cache with the operating\nsystem's protection.",[141,1061,1063],{"className":238,"code":1062,"language":240,"meta":146,"style":146},"def describe_cache() -> dict:\n    cache = load_cache()\n    app = msal.PublicClientApplication(CLIENT_ID, authority=AUTHORITY, token_cache=cache)\n    return {\n        \"cache_file\": str(CACHE_PATH),\n        \"accounts\": [a.get(\"username\") for a in app.get_accounts()],\n        \"private\": CACHE_PATH.exists() and oct(CACHE_PATH.stat().st_mode & 0o777) == \"0o600\",\n    }\n\nprint(describe_cache())\n",[19,1064,1065,1079,1087,1113,1120,1136,1162,1207,1212,1216],{"__ignoreMap":146},[150,1066,1067,1069,1072,1075,1077],{"class":81,"line":152},[150,1068,365],{"class":247},[150,1070,1071],{"class":368}," describe_cache",[150,1073,1074],{"class":251},"() -> ",[150,1076,375],{"class":273},[150,1078,378],{"class":251},[150,1080,1081,1083,1085],{"class":81,"line":255},[150,1082,590],{"class":251},[150,1084,387],{"class":247},[150,1086,725],{"class":251},[150,1088,1089,1091,1093,1095,1097,1099,1101,1103,1105,1107,1109,1111],{"class":81,"line":263},[150,1090,731],{"class":251},[150,1092,387],{"class":247},[150,1094,736],{"class":251},[150,1096,297],{"class":273},[150,1098,220],{"class":251},[150,1100,743],{"class":393},[150,1102,387],{"class":247},[150,1104,317],{"class":273},[150,1106,220],{"class":251},[150,1108,752],{"class":393},[150,1110,387],{"class":247},[150,1112,757],{"class":251},[150,1114,1115,1117],{"class":81,"line":270},[150,1116,479],{"class":247},[150,1118,1119],{"class":251}," {\n",[150,1121,1122,1125,1127,1129,1131,1133],{"class":81,"line":294},[150,1123,1124],{"class":159},"        \"cache_file\"",[150,1126,910],{"class":251},[150,1128,713],{"class":273},[150,1130,430],{"class":251},[150,1132,555],{"class":273},[150,1134,1135],{"class":251},"),\n",[150,1137,1138,1141,1144,1147,1150,1153,1156,1159],{"class":81,"line":314},[150,1139,1140],{"class":159},"        \"accounts\"",[150,1142,1143],{"class":251},": [a.get(",[150,1145,1146],{"class":159},"\"username\"",[150,1148,1149],{"class":251},") ",[150,1151,1152],{"class":247},"for",[150,1154,1155],{"class":251}," a ",[150,1157,1158],{"class":247},"in",[150,1160,1161],{"class":251}," app.get_accounts()],\n",[150,1163,1164,1167,1169,1171,1174,1177,1180,1182,1184,1187,1190,1193,1196,1198,1201,1204],{"class":81,"line":340},[150,1165,1166],{"class":159},"        \"private\"",[150,1168,910],{"class":251},[150,1170,555],{"class":273},[150,1172,1173],{"class":251},".exists() ",[150,1175,1176],{"class":247},"and",[150,1178,1179],{"class":273}," oct",[150,1181,430],{"class":251},[150,1183,555],{"class":273},[150,1185,1186],{"class":251},".stat().st_mode ",[150,1188,1189],{"class":247},"&",[150,1191,1192],{"class":247}," 0o",[150,1194,1195],{"class":273},"777",[150,1197,1149],{"class":251},[150,1199,1200],{"class":247},"==",[150,1202,1203],{"class":159}," \"0o600\"",[150,1205,1206],{"class":251},",\n",[150,1208,1209],{"class":81,"line":357},[150,1210,1211],{"class":251},"    }\n",[150,1213,1214],{"class":81,"line":362},[150,1215,267],{"emptyLinePlaceholder":266},[150,1217,1218,1221],{"class":81,"line":381},[150,1219,1220],{"class":273},"print",[150,1222,1223],{"class":251},"(describe_cache())\n",[10,1225,1226],{},"Use a dedicated account for the job rather than a person's own. A service account whose password is not rotated by a\ndeparting employee — and whose access is limited to the report library — is much easier to reason about. Check it against\nyour organisation's conditional access policies: a policy that requires a compliant device or periodic re-authentication\nwill eventually force a new interactive sign-in, and it is better to know that schedule in advance.",[136,1228,1230],{"id":1229},"step-4-get-an-app-only-token-for-file-content","Step 4 — Get an app-only token for file content",[10,1232,1233],{},"Downloading a workbook to read with pandas, or uploading a finished report, uses the file endpoints, which accept\napplication permissions. There is no user and no cache to maintain: a confidential client exchanges its secret for a token\non every run, and MSAL caches it in memory for the life of the process.",[141,1235,1237],{"className":238,"code":1236,"language":240,"meta":146,"style":146},"def get_app_token() -> str:\n    app = msal.ConfidentialClientApplication(\n        CLIENT_ID,\n        authority=AUTHORITY,\n        client_credential=os.environ.get(\"GRAPH_CLIENT_SECRET\", \"set-me\"),\n    )\n    result = app.acquire_token_for_client(scopes=[\"https:\u002F\u002Fgraph.microsoft.com\u002F.default\"])\n    if \"access_token\" not in result:\n        raise RuntimeError(f\"app token failed: {result.get('error')}: {result.get('error_description')}\")\n    return result[\"access_token\"]\n",[19,1238,1239,1252,1261,1268,1279,1299,1304,1325,1337,1376],{"__ignoreMap":146},[150,1240,1241,1243,1246,1248,1250],{"class":81,"line":152},[150,1242,365],{"class":247},[150,1244,1245],{"class":368}," get_app_token",[150,1247,1074],{"class":251},[150,1249,713],{"class":273},[150,1251,378],{"class":251},[150,1253,1254,1256,1258],{"class":81,"line":255},[150,1255,731],{"class":251},[150,1257,387],{"class":247},[150,1259,1260],{"class":251}," msal.ConfidentialClientApplication(\n",[150,1262,1263,1266],{"class":81,"line":263},[150,1264,1265],{"class":273},"        CLIENT_ID",[150,1267,1206],{"class":251},[150,1269,1270,1273,1275,1277],{"class":81,"line":270},[150,1271,1272],{"class":393},"        authority",[150,1274,387],{"class":247},[150,1276,317],{"class":273},[150,1278,1206],{"class":251},[150,1280,1281,1284,1286,1289,1292,1294,1297],{"class":81,"line":294},[150,1282,1283],{"class":393},"        client_credential",[150,1285,387],{"class":247},[150,1287,1288],{"class":251},"os.environ.get(",[150,1290,1291],{"class":159},"\"GRAPH_CLIENT_SECRET\"",[150,1293,220],{"class":251},[150,1295,1296],{"class":159},"\"set-me\"",[150,1298,1135],{"class":251},[150,1300,1301],{"class":81,"line":314},[150,1302,1303],{"class":251},"    )\n",[150,1305,1306,1308,1310,1313,1315,1317,1320,1323],{"class":81,"line":340},[150,1307,763],{"class":251},[150,1309,387],{"class":247},[150,1311,1312],{"class":251}," app.acquire_token_for_client(",[150,1314,394],{"class":393},[150,1316,387],{"class":247},[150,1318,1319],{"class":251},"[",[150,1321,1322],{"class":159},"\"https:\u002F\u002Fgraph.microsoft.com\u002F.default\"",[150,1324,815],{"class":251},[150,1326,1327,1329,1331,1333,1335],{"class":81,"line":357},[150,1328,406],{"class":247},[150,1330,875],{"class":159},[150,1332,412],{"class":247},[150,1334,415],{"class":247},[150,1336,825],{"class":251},[150,1338,1339,1341,1343,1345,1347,1350,1352,1354,1356,1358,1360,1362,1364,1366,1368,1370,1372,1374],{"class":81,"line":362},[150,1340,424],{"class":247},[150,1342,427],{"class":273},[150,1344,430],{"class":251},[150,1346,433],{"class":247},[150,1348,1349],{"class":159},"\"app token failed: ",[150,1351,329],{"class":328},[150,1353,900],{"class":251},[150,1355,903],{"class":159},[150,1357,447],{"class":251},[150,1359,334],{"class":328},[150,1361,910],{"class":159},[150,1363,329],{"class":328},[150,1365,900],{"class":251},[150,1367,444],{"class":159},[150,1369,447],{"class":251},[150,1371,334],{"class":328},[150,1373,452],{"class":159},[150,1375,291],{"class":251},[150,1377,1378,1380,1382,1384],{"class":81,"line":381},[150,1379,479],{"class":247},[150,1381,932],{"class":251},[150,1383,935],{"class":159},[150,1385,354],{"class":251},[10,1387,940,1388,1391,1392,1396,1397,1399],{},[19,1389,1390],{},".default"," scope means \"whatever application permissions an administrator consented to for this app\"; you cannot\nrequest individual application permissions at token time. The same pattern is used for publishing in\n",[33,1393,1395],{"href":1394},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-sharepoint-with-python\u002F","Upload an Excel Report to SharePoint with Python",",\nwhere ",[19,1398,208],{}," limits the app to one site.",[136,1401,1403],{"id":1402},"step-5-verify-the-token-before-doing-real-work","Step 5 — Verify the token before doing real work",[10,1405,1406],{},"A token that was issued is not necessarily a token that works for your file. Make one cheap call at the start of the job\nand turn the common failures into messages a person can act on.",[141,1408,1410],{"className":238,"code":1409,"language":240,"meta":146,"style":146},"import requests\n\nGRAPH = \"https:\u002F\u002Fgraph.microsoft.com\u002Fv1.0\"\n\ndef check_delegated(token: str) -> str:\n    response = requests.get(f\"{GRAPH}\u002Fme\", headers={\"Authorization\": f\"Bearer {token}\"}, timeout=30)\n    if response.status_code == 401:\n        raise RuntimeError(\"token rejected — expired or for the wrong tenant; re-run the interactive sign-in\")\n    if response.status_code == 403:\n        raise RuntimeError(\"signed in, but consent for the requested permissions is missing\")\n    response.raise_for_status()\n    return response.json()[\"userPrincipalName\"]\n",[19,1411,1412,1418,1422,1432,1436,1454,1518,1532,1545,1558,1571,1576],{"__ignoreMap":146},[150,1413,1414,1416],{"class":81,"line":152},[150,1415,248],{"class":247},[150,1417,166],{"class":251},[150,1419,1420],{"class":81,"line":255},[150,1421,267],{"emptyLinePlaceholder":266},[150,1423,1424,1427,1429],{"class":81,"line":263},[150,1425,1426],{"class":273},"GRAPH",[150,1428,277],{"class":247},[150,1430,1431],{"class":159}," \"https:\u002F\u002Fgraph.microsoft.com\u002Fv1.0\"\n",[150,1433,1434],{"class":81,"line":270},[150,1435,267],{"emptyLinePlaceholder":266},[150,1437,1438,1440,1443,1446,1448,1450,1452],{"class":81,"line":294},[150,1439,365],{"class":247},[150,1441,1442],{"class":368}," check_delegated",[150,1444,1445],{"class":251},"(token: ",[150,1447,713],{"class":273},[150,1449,710],{"class":251},[150,1451,713],{"class":273},[150,1453,378],{"class":251},[150,1455,1456,1459,1461,1464,1466,1468,1470,1472,1474,1477,1479,1482,1484,1486,1489,1491,1493,1496,1498,1501,1503,1505,1508,1511,1513,1516],{"class":81,"line":314},[150,1457,1458],{"class":251},"    response ",[150,1460,387],{"class":247},[150,1462,1463],{"class":251}," requests.get(",[150,1465,433],{"class":247},[150,1467,452],{"class":159},[150,1469,329],{"class":328},[150,1471,1426],{"class":273},[150,1473,334],{"class":328},[150,1475,1476],{"class":159},"\u002Fme\"",[150,1478,220],{"class":251},[150,1480,1481],{"class":393},"headers",[150,1483,387],{"class":247},[150,1485,329],{"class":251},[150,1487,1488],{"class":159},"\"Authorization\"",[150,1490,910],{"class":251},[150,1492,433],{"class":247},[150,1494,1495],{"class":159},"\"Bearer ",[150,1497,329],{"class":328},[150,1499,1500],{"class":251},"token",[150,1502,334],{"class":328},[150,1504,452],{"class":159},[150,1506,1507],{"class":251},"}, ",[150,1509,1510],{"class":393},"timeout",[150,1512,387],{"class":247},[150,1514,1515],{"class":273},"30",[150,1517,291],{"class":251},[150,1519,1520,1522,1525,1527,1530],{"class":81,"line":340},[150,1521,406],{"class":247},[150,1523,1524],{"class":251}," response.status_code ",[150,1526,1200],{"class":247},[150,1528,1529],{"class":273}," 401",[150,1531,378],{"class":251},[150,1533,1534,1536,1538,1540,1543],{"class":81,"line":357},[150,1535,424],{"class":247},[150,1537,427],{"class":273},[150,1539,430],{"class":251},[150,1541,1542],{"class":159},"\"token rejected — expired or for the wrong tenant; re-run the interactive sign-in\"",[150,1544,291],{"class":251},[150,1546,1547,1549,1551,1553,1556],{"class":81,"line":362},[150,1548,406],{"class":247},[150,1550,1524],{"class":251},[150,1552,1200],{"class":247},[150,1554,1555],{"class":273}," 403",[150,1557,378],{"class":251},[150,1559,1560,1562,1564,1566,1569],{"class":81,"line":381},[150,1561,424],{"class":247},[150,1563,427],{"class":273},[150,1565,430],{"class":251},[150,1567,1568],{"class":159},"\"signed in, but consent for the requested permissions is missing\"",[150,1570,291],{"class":251},[150,1572,1573],{"class":81,"line":403},[150,1574,1575],{"class":251},"    response.raise_for_status()\n",[150,1577,1578,1580,1583,1586],{"class":81,"line":421},[150,1579,479],{"class":247},[150,1581,1582],{"class":251}," response.json()[",[150,1584,1585],{"class":159},"\"userPrincipalName\"",[150,1587,354],{"class":251},[10,1589,1590,1593],{},[19,1591,1592],{},"\u002Fme"," only exists for delegated tokens, which makes it a neat check that the job is not accidentally using the app-only\npath for workbook calls. A workbook endpoint called with an app-only token fails with an error even though the same token\ndownloads the file perfectly — a confusing combination unless you know the rule from the start of this page.",[39,1595,48,1600,48,1603,48,1606,48,1609,48,1614,48,1617,48,1620,48,1624,48,1626,48,1629,48,1634,48,1638,48,1642,48,1646,48,1650,48,1652,48,1655,48,1660,48,1664,48,1668,48,1671,48,1674,48,1676,48,1679,48,1684,48,1688,48,1692,48,1695,48,1698],{"viewBox":1596,"role":42,"ariaLabelledBy":1597,"xmlns":46,"style":47},"0 0 760 282",[1598,1599],"auth-check-t","auth-check-d",[50,1601,1602],{"id":1598},"Reading the result of a token check call",[54,1604,1605],{"id":1599},"A 200 response from the me endpoint means the delegated token is valid. A 401 means the token is expired or issued for the wrong tenant and a fresh sign-in is needed. A 403 means sign-in worked but consent for the permissions is missing.",[58,1607],{"x":60,"y":60,"width":61,"height":1608,"fill":63},"282",[58,1610],{"x":1611,"y":66,"width":1612,"height":1613,"rx":967,"fill":107,"stroke":108,"style":72},"230","300","52",[74,1615,1616],{"x":131,"y":1613,"style":997},"GET \u002Fme with the token",[81,1618],{"x1":131,"y1":978,"x2":131,"y2":1619,"stroke":108,"style":72},"100",[81,1621],{"x1":1622,"y1":1619,"x2":1623,"y2":1619,"stroke":108,"style":72},"133.3","626.7",[81,1625],{"x1":1622,"y1":1619,"x2":1622,"y2":98,"stroke":108,"style":72},[987,1627],{"points":1628,"fill":990},"133.3,127 128.3,118 138.3,118",[58,1630],{"x":66,"y":1631,"width":1632,"height":1633,"rx":967,"fill":70,"stroke":71,"style":72},"128","226.7","104",[74,1635,1637],{"x":1622,"y":68,"style":1636},"font-size:11px;font-weight:600;fill:var(--teal-ink,#0b6157);text-anchor:middle","200 OK",[74,1639,1641],{"x":1622,"y":1640,"style":1036},"174","token works",[74,1643,1645],{"x":1622,"y":1644,"style":90},"196","continue with",[74,1647,1649],{"x":1622,"y":1648,"style":90},"216","workbook calls",[81,1651],{"x1":131,"y1":1619,"x2":131,"y2":98,"stroke":108,"style":72},[987,1653],{"points":1654,"fill":990},"380,127 375,118 385,118",[58,1656],{"x":1657,"y":1631,"width":1632,"height":1633,"rx":967,"fill":1658,"stroke":1659,"style":72},"266.7","#fdefd8","var(--gold,#b4740a)",[74,1661,1663],{"x":131,"y":68,"style":1662},"font-size:11px;font-weight:600;fill:var(--gold-ink,#7a4e06);text-anchor:middle","401 Unauthorized",[74,1665,1667],{"x":131,"y":1640,"style":1666},"font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","bad token",[74,1669,1670],{"x":131,"y":1644,"style":90},"expired or wrong tenant:",[74,1672,1673],{"x":131,"y":1648,"style":90},"sign in again",[81,1675],{"x1":1623,"y1":1619,"x2":1623,"y2":98,"stroke":108,"style":72},[987,1677],{"points":1678,"fill":990},"626.7,127 621.7,118 631.7,118",[58,1680],{"x":1681,"y":1631,"width":1632,"height":1633,"rx":967,"fill":1682,"stroke":1683,"style":72},"513.3","#fee8f2","var(--accent,#f43f8f)",[74,1685,1687],{"x":1623,"y":68,"style":1686},"font-size:11px;font-weight:600;fill:var(--accent-ink,#be185d);text-anchor:middle","403 Forbidden",[74,1689,1691],{"x":1623,"y":1640,"style":1690},"font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","no consent",[74,1693,1694],{"x":1623,"y":1644,"style":90},"grant Files.ReadWrite",[74,1696,1697],{"x":1623,"y":1648,"style":90},"to the app",[74,1699,1701],{"x":131,"y":1700,"style":133},"266","one cheap call turns vague failures into specific next steps",[141,1703,1705],{"className":238,"code":1704,"language":240,"meta":146,"style":146},"token = get_delegated_token()\nprint(\"signed in as\", check_delegated(token))\n",[19,1706,1707,1717],{"__ignoreMap":146},[150,1708,1709,1712,1714],{"class":81,"line":152},[150,1710,1711],{"class":251},"token ",[150,1713,387],{"class":247},[150,1715,1716],{"class":251}," get_delegated_token()\n",[150,1718,1719,1721,1723,1726],{"class":81,"line":255},[150,1720,1220],{"class":273},[150,1722,430],{"class":251},[150,1724,1725],{"class":159},"\"signed in as\"",[150,1727,1728],{"class":251},", check_delegated(token))\n",[136,1730,1732],{"id":1731},"common-pitfalls","Common pitfalls",[1734,1735,1736,1752],"table",{},[1737,1738,1739],"thead",{},[1740,1741,1742,1746,1749],"tr",{},[1743,1744,1745],"th",{},"Symptom",[1743,1747,1748],{},"Cause",[1743,1750,1751],{},"Fix",[1753,1754,1755,1767,1784,1797,1808,1822],"tbody",{},[1740,1756,1757,1761,1764],{},[1758,1759,1760],"td",{},"Workbook calls fail with an app-only token that downloads files fine",[1758,1762,1763],{},"Workbook endpoints do not support application permissions",[1758,1765,1766],{},"Use a delegated token for ranges, tables and calculation",[1740,1768,1769,1775,1778],{},[1758,1770,1771,1774],{},[19,1772,1773],{},"AADSTS7000218"," or public client error on device flow",[1758,1776,1777],{},"Public client flows not enabled",[1758,1779,1780,1781,1783],{},"Enable ",[14,1782,198],{}," on the app registration",[1740,1785,1786,1789,1792],{},[1758,1787,1788],{},"Scheduled job hangs",[1758,1790,1791],{},"Device flow started in a non-interactive run",[1758,1793,1794,1795],{},"Fail fast unless ",[19,1796,947],{},[1740,1798,1799,1802,1805],{},[1758,1800,1801],{},"Silent acquisition suddenly returns nothing",[1758,1803,1804],{},"Refresh token revoked: password reset, policy, or long inactivity",[1758,1806,1807],{},"Sign in interactively again; alert on this failure",[1740,1809,1810,1816,1819],{},[1758,1811,1812,1815],{},[19,1813,1814],{},"403"," on a SharePoint file with a valid delegated token",[1758,1817,1818],{},"The account has no access to that library",[1758,1820,1821],{},"Grant the service account access to the site or file",[1740,1823,1824,1827,1830],{},[1758,1825,1826],{},"Cache readable by other users",[1758,1828,1829],{},"Default file permissions",[1758,1831,1832,1833,1836],{},"Restrict to ",[19,1834,1835],{},"0600"," or use a keyring-backed cache",[136,1838,1840],{"id":1839},"performance-and-scale-notes","Performance and scale notes",[10,1842,1843,1844,1847,1848,1851,1852,228],{},"Silent token acquisition from a warm cache is a local operation until the access token expires, typically after about an\nhour; then one refresh request is made. Create the MSAL application object once per process and reuse it, so that its\nin-memory cache avoids repeated file reads. For many workbooks in one run, reuse a single ",[19,1845,1846],{},"requests.Session"," with the\n",[19,1849,1850],{},"Authorization"," header set, and refresh the header only when a call returns ",[19,1853,1854],{},"401",[136,1856,1858],{"id":1857},"conclusion","Conclusion",[10,1860,1861],{},"Pick the token by the endpoint. File content — downloading a workbook for pandas, uploading a finished report — works with an\napp-only token from client credentials. The workbook API that edits ranges, appends table rows and recalculates needs a delegated\ntoken, so use MSAL's device code flow once, persist its token cache as a protected credential, and acquire tokens silently on\nevery scheduled run, failing fast with a clear message when the cached sign-in is gone. Verify with a cheap call before doing real\nwork, and use a dedicated account whose access is limited to the files the job needs.",[136,1863,1865],{"id":1864},"frequently-asked-questions","Frequently asked questions",[10,1867,1868,1871],{},[14,1869,1870],{},"Why can't I use client credentials for the Excel workbook API?","\nMicrosoft's permission tables for the workbook endpoints list application permissions as not supported. Those calls act on a workbook session on behalf of a user, so they need a delegated token.",[10,1873,1874,1877],{},[14,1875,1876],{},"How long does a cached sign-in last?","\nAccess tokens last about an hour and are refreshed automatically. The refresh token behind them is long-lived but can be revoked by a password reset, conditional access policy, administrator action or a long period without use, so monitor for silent-acquisition failures.",[10,1879,1880,1883],{},[14,1881,1882],{},"Can I use a username and password instead of the device code?","\nMSAL supports the resource owner password flow, but Microsoft discourages it and it fails for accounts with multi-factor authentication. The device code flow with a cached token is the more robust choice.",[10,1885,1886,1889],{},[14,1887,1888],{},"Do I need admin consent for Files.ReadWrite?","\nUsually not for the delegated Files.ReadWrite permission, which users can consent to themselves unless the tenant restricts user consent. Application permissions always need an administrator.",[136,1891,1893],{"id":1892},"related","Related",[175,1895,1896,1902,1909,1916,1923],{},[178,1897,1898,1899,1901],{},"Up one level: ",[33,1900,36],{"href":35}," — the full workflow for workbooks in OneDrive and SharePoint.",[178,1903,1904,1908],{},[33,1905,1907],{"href":1906},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fread-an-excel-file-from-sharepoint-or-onedrive-into-pandas\u002F","Read an Excel File from SharePoint or OneDrive into pandas"," — the app-only file path in use.",[178,1910,1911,1915],{},[33,1912,1914],{"href":1913},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fread-and-write-excel-online-ranges-with-microsoft-graph\u002F","Read and Write Excel Online Ranges with Microsoft Graph"," — the first delegated workbook calls.",[178,1917,1918,1922],{},[33,1919,1921],{"href":1920},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-microsoft-graph-api\u002F","Send an Excel Report with the Microsoft Graph API"," — another Graph workload with its own permissions.",[178,1924,1925,1929],{},[33,1926,1928],{"href":1927},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fkeep-excel-report-settings-in-a-config-file\u002F","Keep Excel Report Settings in a Config File"," — keeping IDs and paths out of code.",[1931,1932,1933],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":146,"searchDepth":255,"depth":255,"links":1935},[1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946],{"id":138,"depth":255,"text":139},{"id":231,"depth":255,"text":232},{"id":502,"depth":255,"text":503},{"id":1047,"depth":255,"text":1048},{"id":1229,"depth":255,"text":1230},{"id":1402,"depth":255,"text":1403},{"id":1731,"depth":255,"text":1732},{"id":1839,"depth":255,"text":1840},{"id":1857,"depth":255,"text":1858},{"id":1864,"depth":255,"text":1865},{"id":1892,"depth":255,"text":1893},"2026-09-17","The Excel workbook API accepts only delegated tokens, while file downloads work app-only. Set up MSAL device-code sign-in with a persistent token cache for scheduled jobs, and client credentials where they are allowed.","md",[1951,1953,1955,1957],{"q":1870,"a":1952},"Microsoft's permission tables for the workbook endpoints list application permissions as not supported. Those calls act on a workbook session on behalf of a user, so they need a delegated token.",{"q":1876,"a":1954},"Access tokens last about an hour and are refreshed automatically. The refresh token behind them is long-lived but can be revoked by a password reset, conditional access policy, administrator action or a long period without use, so monitor for silent-acquisition failures.",{"q":1882,"a":1956},"MSAL supports the resource owner password flow, but Microsoft discourages it and it fails for accounts with multi-factor authentication. The device code flow with a cached token is the more robust choice.",{"q":1888,"a":1958},"Usually not for the delegated Files.ReadWrite permission, which users can consent to themselves unless the tenant restricts user consent. Application permissions always need an administrator.","authenticate to microsoft graph with msal for excel automation",null,{"breadcrumb":1962},[1963,1966,1969],{"name":1964,"item":1965},"Home","\u002F",{"name":1967,"item":1968},"Automating Reporting Workflows","\u002Fautomating-reporting-workflows\u002F",{"name":36,"item":35},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fauthenticate-to-microsoft-graph-with-msal-for-excel-automation",{"title":5,"description":1972},"Get Microsoft Graph tokens in Python for Excel Online work: delegated Files.ReadWrite via MSAL device code flow, a persistent token cache for unattended runs, and app-only tokens for file content.","authenticate-to-microsoft-graph-with-msal-for-excel-automation","automating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fauthenticate-to-microsoft-graph-with-msal-for-excel-automation\u002Findex",[1976],"excel-online","how-to","4aLAJfsd5eW1Mhf0yiz0ei53om5JZJgpPYmTYfUlbiA",[1980,1984],{"title":1981,"path":1982,"stem":1983,"children":-1},"Append Rows to an Excel Online Table with Microsoft Graph","\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fappend-rows-to-an-excel-online-table-with-microsoft-graph","automating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fappend-rows-to-an-excel-online-table-with-microsoft-graph\u002Findex",{"title":1985,"path":1986,"stem":1987,"children":-1},"Call Excel Worksheet Functions Through Microsoft Graph","\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fcall-excel-worksheet-functions-through-microsoft-graph","automating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fcall-excel-worksheet-functions-through-microsoft-graph\u002Findex",[1989,1991],{"path":1990,"title":36},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph",{"path":1992,"title":1993},"\u002Fautomating-reporting-workflows","Automating Reporting Workflows with Python and Excel",[],[1996,1998,2001,2004,2005,2006],{"path":1997,"title":1921,"kind":1977},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-microsoft-graph-api",{"path":1999,"title":2000,"kind":1977},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-a-chart-to-an-excel-online-workbook-with-microsoft-graph","Add a Chart to an Excel Online Workbook with Microsoft Graph",{"path":2002,"title":2003,"kind":1977},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-rename-and-delete-worksheets-with-microsoft-graph","Add, Rename and Delete Worksheets with Microsoft Graph",{"path":1982,"title":1981,"kind":1977},{"path":1986,"title":1985,"kind":1977},{"path":2007,"title":2008,"kind":1977},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fcopy-a-template-workbook-in-sharepoint-with-microsoft-graph","Copy a Template Workbook in SharePoint with Microsoft Graph",1790972118914]